Как сделать модуль числа в ассемблере

Добавил пользователь Alex
Обновлено: 05.10.2024

На этом уроке мы будем разбираться с арифметическими инструкциями в ассемблере на примере INC, DEC, ADD, SUB и пр.

Инструкция INC

Синтаксис инструкции INC:

Инструкция DEC

Синтаксис инструкции DEC:

Инструкции ADD и SUB

Синтаксис инструкций ADD и SUB:

ADD/SUB место_назначения, источник

Инструкции ADD/SUB могут выполняться между:

регистром и регистром;

памятью и регистром;

регистром и памятью;

памятью и константами.

Однако, как и для других инструкций, операции типа память-в-память невозможны с использованием инструкций ADD/SUB. Операции ADD или SUB устанавливают или сбрасывают флаги переполнения и переноса.

В следующем примере мы спрашиваем у пользователя два числа, сохраняем их в регистрах EAX и EBX, затем выполняем операцию сложения, сохраняем результат в ячейке памяти res и выводим его на экран:

Результат выполнения программы:

Enter a digit:
3
Please enter a second digit:
4
The sum is:
7

Ниже рассмотрен пример, в котором за счет того, что значения переменных для арифметических выражений прописаны в самом коде программы, можно получить код программы короче и проще:

Результат выполнения программы:

Инструкции MUL и IMUL

Есть 2 инструкции для умножения двоичных данных:

Обе инструкции влияют на флаги переноса и переполнения.

Синтаксис инструкций MUL/IMUL:

Множимое в обоих случаях будет находиться в аккумуляторе, в зависимости от размера множимого и множителя, и результат умножения также сохраняется в двух регистрах, в зависимости от размера операндов.

Рассмотрим 3 разных сценария:

Сценарий №1: Перемножаются 2 значения типа byte. Множимое находится в регистре AL, а множителем является значение типа byte в памяти или в другом регистре. Результат произведения находится в AX. Старшие 8 бит произведения хранятся в AH, а младшие 8 бит хранятся в AL:


Сценарий №2: Перемножаются 2 значения типа word. Множимое должно находиться в регистре AX, а множителем является значение типа word в памяти или в другом регистре. Например, для такой инструкции, как MUL DX , вы должны сохранить множитель в DX, а множимое — в AX. В результате получится значение типа doubleword, для которого понадобятся два регистра. Часть высшего порядка (крайняя слева) сохраняется в DX, а часть нижнего порядка (крайняя справа) сохраняется в AX:


Сценарий №3: Перемножаются 2 значения типа doubleword. Множимое должно находиться в EAX, а множителем является значение типа doubleword, хранящееся в памяти или в другом регистре. Результат умножения сохраняется в регистрах EDX и EAX. Биты старшего порядка сохраняются в регистре EDX, а биты младшего порядка сохраняются в регистре EAX:

.data
summand_1db ? значения в summand_1и summand_2
summandj? db ? :нужно внести
sum_w label word
sum_b db 0
carry db 0
.code
add_unsign proc
mov al ,summand_2
add al ,summand_1mov sumji.al
jnc end_p :проверка на переполнение
adc carry,0
end_p: ret
add_unsign endp

Программа учитывает возможное переполнение результата. Сложение двоичных чисел большей размерности (2/4 байта) выполняется аналогично. Для этого необходимо заменить директивы DB на DW/DD и регистр AL на АХ/ЕАХ.

Сложение чисел размером N байт без учета знака

.data
summand_1db ? ;первое слагаемое
N=$-surranand_1;длина в байтах значений summand_1и summand_2
carry db 0 :перенос сложения последних байтов
summand_2 db ? :второе слагаемое
.code
add_unsign_N proc
mov cl. N
хог si.si cycl: mov al ,summand_2[si]
adc summand_l[si].al
inc si
loop cycl
jnc end_p ;проверка на переполнение
adc carry. 0
end_p: ret
add_unsign_N endp

Программа учитывает возможное переполнение результата. Сегмент данных может быть задан, например, так:

.data
summand_1db 0.34.56.78.250 ; первое слагаемое
N=$-summand_1:длина в байтах значений summand_1и summand_2
carry db 0 ;перенос сложения последних байт
summand_2 db 0.43.65.230.250 : второе слагаемое

Далее при рассмотрении программы деления многобайтных двоичных чисел нам понадобится макрокоманда сложения без учета знака чисел размером N байт (порядок следования байтов не соответствует порядку следования байтов на процессорах Intel, то есть старший байт находится по младшему адресу). Приведем ее.

Сложение без учета знака чисел размером N байт (макрокоманда)

Сложение чисел размером 1 байт с учетом знака

Программа учитывает возможное переполнение результата и перенос в старшие разряды. Для этого отслеживаются условия, задаваемые флагами, и выполняются действия:

  • CF=0F=0 — результат правильный и является положительным числом;
  • CF=1 0F=0 — результат правильный и является отрицательным числом;
  • CF=0F=1 — результат неправильный и является положительным числом, хотя правильный результат должен быть отрицательным (для корректировки необходимо увеличить размер результата в два раза и заполнить это расширение нулевым значением);
  • CF=0 0F=1 — результат неправильный и является отрицательным числом, хотя правильный результат должен быть положительным (для корректировки необходимо увеличить размер результата в два раза и произвести расширение знака).

Сложение чисел со знаком большей размерности (2/4 байта) выполняется аналогично, для этого необходимо внести изменения в соответствующие фрагменты программы. В частности, необходимо заменить директивы DB на DW/DD и регистр AL на АХ/ЕАХ.

Сложение с учетом знака чисел размером N байт

Сегмент данных может быть задан, например, так:

.data
summand_1db 32,126,-120 ;первое слагаемое
N=$-summand_1;длина в байтах значений summand_1и sumniand_2
carry db 0 расширение знака
summand_2 db 126,125,-120 ;второе слагаемое

Программа учитывает возможное переполнение результата. Обратите внимание на порядок задания значений слагаемого. Если слагаемое положительное, то проблем нет. Отрицательное слагаемое размером N байт для своего задания требует некоторых допущений. Старший байт отрицательного слагаемого задается со знаком и в процессе трансляции будет преобразован в значение, являющееся двоичным дополнением исходного значения. Остальные байты в своем исходном виде должны быть частью доbxя. Поэтому для работы с числами со знаком удобно иметь программу, которая бы выполняла вычисление значения модуля отрицательного числа и, наоборот, по значению модуля вычисляла его дополнение.

Вычисление дополнения числа размером N байт

.code calc_complement proc
хог si,si
neg byte ptr [bx] дополнение первого байта
cmp byte ptr [bx],0 ;нулевой операнд - особый случай
jne short $+3
stc установить cf, так как есть перенос
dec ex
jcxz @@ml ;для однобайтного операнда
@@cycl: iпс si
not byte ptr [bx][si]
adc byte ptr [bx][si],0
loop @@cycl
@@ml: ret calc_complement endp

Для значений размерностью 1/2/4 байта дополнение можно получать с помощью одной команды NEG:

Для значений N байт необходимо реализовывать алгоритм. Дополнение первого байта необходимо вычислять с учетом того, что он может быть нулевым. Попытка получить его дополнение с помощью команды NEG обречена на провал. Флаг CF в этом случае также должен устанавливаться программно. Подумайте, почему?

Вычисление модуля числа размером N байт

.code
calc_abs proc определим знак операнда
mov si.cx
dec si
test byte ptr [bx][si],80h проверяем знак операнда
jz @@exit ;число положительное
call calc_complement @@exit:ret
calc_abs endp

Вычитание двоичных чисел

Вычитание чисел размером 1 байт без учета знака

.data значения в minuend и deduction нужно задать
minuend db ? уменьшаемое
deduction db ? ;вычитаемое
.code
sub_unsign proc
mov al .deduction
subminuend.al :оценить результат на случай уменьшаемое уменьшаемое Вычитание чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD и регистр AL на АХ/ЕАХ.

Вычитание чисел размером N байт без учета знака

.data значения в minuend и deduction нужно внести
minuenddb ? уменьшаемое
N=$-minuend ;длина в байтах значений minuend и deduction '.
deduction db ? :вычитаемое
.code
sub_unsign_N proc
mov cl.N
xor si,si cycl: moval ,deduction[si]
sbbminuend[si].al
jnc @@ml
negminuendtsi] @@ml: inc si
loop cycl
ret sub_uns1gn_N endp

Программа учитывает возможный заем из старших разрядов. Длина уменьшаемого должна быть не меньше длины вычитаемого, недостающие разряды вычитаемого должны быть нулевыми. В любом случае, результат — абсолютное значение.
Сегмент данных может быть задан, например, так:

.data
N equ5 ;длина в байтах значений minuend и deduction
minuenddb 30.43.65.230,250 уменьшаемое
deduction db 45.34.65.78.250 ;вычитаемое

Вычитание чисел размером 1 байт с учетом знака

.data значения в minuend и deduction нужно внести
N equ 2 :длина в байтах результата в ситуации расширения знака для получения его модуля
minuend db ? -.уменьшаемое
carry db 0 расширение знака
deduction db ? :вычитаемое
.code
sub_sign proc
mov al .deduction
subminuend.al ;оценить результат:
jnc no_carry :нет заема обрабатываем ситуацию заема из старшего разряда - получаем модуль (если нужно)
neg minuend
jmp end_p
no_carry: jns no_sign обрабатываем ситуацию получения отрицательного результата - получаем модуль (если нужно)
neg minuend
jmp end_p
no_sign: jno no_overflow обрабатываем ситуацию переполнения - получаем модуль (если нужно).
расширить результат знаком - получаем модуль (если нужно):
mov carry.0ffh
call calc abs no_overflow:
endjr ret sub_sign endp

Вычитание чисел размером N байт с учетом знака

.data :значения в minuend и deduction нужно внести
minuenddb ? уменьшаемое
lenjninuend=$-minuend ;длина в байтах уменьшаемого и вычитаемого
carry db 0 расширение знака
deduction db ? ;вычитаемое
.code
sub_sign_N proc
mov cx.lenjninuend
mov si.O @@ml: mov al,deduction[si]
sbb minuend[si].al
inc si
loop @@ml оценить результат:
jnc no_carry :нет заема
обрабатываем ситуацию заема из старшего разряда - получаем модуль (если нужно) N=1en_minuend+1
mov carry.0ffh
call calc_abs
jmp end_p no_carry: jns no_sign Обрабатываем ситуацию получения отрицательного результата -
:получаем модуль (если нужно) N=1en_minuend
call calc_abs
jmp end_p
no_sign: jno no_overflow
обрабатываем ситуацию переполнения - получаем модуль (если нужно) расширить результат знаком - получаем модуль (если нужно): N=1en_minuend+1
mov carry,0ffh
call catc_abs no_overflow: end_p: ret sub_sign_N endp

Сегмент данных может быть задан, например, так:

.data :значения в minuend и deduction нужно внести
minuend db 25h,0f4h,0eh уменьшаемое
len_minuend=$-minuend ;длина в сайтах уменьшаемого и вычитаемого
carry db 0 ;расширение знака
deduction db 5h,0f4h,0fh :вычитаемое

Далее при рассмотрении программы деления многобайтных двоичных чисел нам понадобится макрокоманда вычитания с учетом знака чисел размером N байт (порядок следования байт не соответствует порядку следования байтов на процессорах Intel, то есть старший байт находится по младшему адресу). Приведем ее.

Вычитание с учетом знака чисел размером N байт (макрокоманда)

push si
mov cl,N
mov si.N-1 cycl: moval .deduction[si]
sbbminuend[si],al : jnc ml
: neg minuend[si] ml: dec si
loop cycl
pop si
endm

Умножение двоичных чисел

В отличие от сложения и вычитания операция умножения реализуется двумя типами команд — учитывающими и не учитывающими знаки операндов.

Умножение чисел размером 1 байт без учета знака

Здесь все достаточно просто и реализуется средствами самого процессора. Проблема состоит лишь в правильном определении размера результата. Произведение чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD, регистр AL на АХ/ЕАХ, регистр АН на DX/EDX.

Умножение чисел размером N и М байт без учета знака

Для умножения чисел размером N и М байт, существует несколько стандартных алгоритмов, описанных в литературе. В этом разделе мы рассмотрим только один из них. В его основе лежит алгоритм умножения неотрицательных целых чисел, предложенный Кнутом.

Умножение N-байтного числа на число размером М байт

Умножение чисел размером 1 байт с учетом знака

Аналогично умножению без знака здесь также все достаточно просто и реализуется средствами самого процессора. Проблема та же — правильное определение размера результата. Произведение чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD, регистр AL на АХ/ЕАХ, регистр АН на DX/EDX. Более того, в отличие от команды MUL команда IMLJL допускает более гибкое расположение операндов.

Умножение чисел размером N и М байт с учетом знака

Как уже не раз отмечалось, система команд микропроцессора содержит два типа команд умножения — с учетом знаков операнда (IMUL) и без него (MUL). При умножении операндов размером 1/2/4 байта учет знака производится автоматически — по состоянию старших (знаковых) битов. Если умножаются числа размером в большее количество байтов, то для получения правильного результата необходимо учитывать знаковые разряды только старших байтов. В основе программы, реализующей алгоритм умножения чисел размером N и М байт с учетом знака, лежит рассмотренная выше процедура умножения чисел произвольной размерности без учета знака.

Умножение N-байтного числа на число размером М байт с учетом знака

.stack 256
.486
.code
calc_complement_r proc
dec cx
mov si,cx
neg byte ptr [bx][si] ;дополнение первого байта
cmp byte ptr [bx][si],0 ;operand=0 - особый случай
jne short $+3
stc ;установить cf, так как есть перенос
jcxz @@exit_cycl ;для однозначного числа
@@cycl: dec si
not byte ptr [bx][si]
adc byte ptr [bx][si],0
loop @@cycl
@@exit_cycl: ret
calc_complement_r endp

mul_unsign_NM macro u,i,v,j,w
local m2,m4,m6
push si
;очистим w
push ds
pop es
xor al,al
lea di,w
mov cx,i+j
rep stosb

;m1
mov bx,j-1 ;j=0..m-1
mov cx,j
m2:
push cx ;вложенные циклы
cmp v[bx],0
je m6
;m3
mov si,i-1 ;i=0..n-1
mov cx,i
mov k,0
m4:
mov al,u[si]
mul byte ptr v[bx]
xor dx,dx
mov dl,w[bx+si+1]
add ax,dx
xor dx,dx
mov dl,k
add ax,dx ;t=(ax) ? временная переменная
push dx
xor dx,dx
div b ;t mod b
mov ah,dl
pop dx
mov k,al
mov w[bx+si+1],ah
;m5
dec si
loop m4
mov al,k
mov w[bx],al
m6:
dec bx
pop cx
loop m2
pop si
endm

sub_sign_N macro minuend,deduction,N
local cycl,m1
;старший байт по младшему адресу
push si
mov cl,N
mov si,N-1
cycl: mov al,deduction[si]
sbb minuend[si],al
; jnc m1
; neg minuend[si]
m1: dec si
loop cycl
pop si
endm

add_unsign_N macro carry,summand_1,summand_2,N
local cycl,end_p
mov cl,N
mov si,N-1
cycl: mov al,summand_2[si]
adc summand_1[si],al
dec si
loop cycl
jnc end_p
adc carry,0
end_p: nop
endm

div_sign_N macro u,N,v,w,r
local m1
;старший байт по младшему адресу
mov r,0
lea si,u ;j=0
xor di,di ;j=0
mov cx,N
xor dx,dx
xor bx,bx
m1: mov ax,256 ;основание с.с.
mul word ptr r ;результат в dx:ax
mov bl,[si]
add ax,bx
div v
;сформировать результат:
mov w[di],al ;частное
mov r,ah ;остаток в r
inc si
inc di
loop m1
;если нужно - получим модуль (уберите знаки комментария)
; mov cx,N ;длина операнда
; lea bx,w
; call calc_abs_r
endm

div_unsign_NM proc
;НАЧ_ПРОГ
;//шаг 1 - нормализация:
;D1 - нормализация
;d:=b/(v[n-1]+1)
xor ax,ax
mov dl,v
inc dl ;vn-1+1
mov ax,b
div dl
mov d,al ;d=b/(v1+1)
;u[n+m…0]:=u[n+m-1…0]*d
mul_unsign_NM u,m,d,1,w
cld
push ds
pop es
lea si,w
lea di,u0
mov cx,m+1
rep movsb
;v[n-1…0]:=v[n-1…0]*d
mul_unsign_NM v,n,d,1,w
cld
push ds
pop es
lea si,w+1
lea di,v
mov cx,n
rep movsb
;//шаг 2 - начальная установка j:
;mm:=m-n; j:=mm
;D2:
mov si,0 ;n=0 (? n=n+m)
;D3:
@@m7:
;//шаг 3 - вычислить частичное частное qq :
;qq:=(u[j+n]*b+u[j+n-1]) / v[n-1]
;rr:=(u[j+n]*b+u[j+n-1]) MOD v[n-1]
@@m1: xor ax,ax
mov al,u0[si]
mul b
shl eax,16
shrd eax,edx,16 ;результат умножения в eax
xor edx,edx
mov dl,u0[si+1]
add eax,edx
shld edx,eax,16 ;восстановили пару dx:ax для деления
xor bx,bx
mov bl,v ;v->bx
div bx
mov qq,ax
mov rr,dx
@@m2:
;проверим выполнение неравенства:
;ДЕЛАТЬ ПОКА tf
;НАЧ_БЛОК_1
;ЕСЛИ qq==b OR qq*v[n-2] > b*rr+ u[j+n-1] ТО
;НАЧ_БЛОК_2
;qq:=qq-1
;rr:=rr+v[n-1]
;ЕСЛИ rrіb ТО tf:=FALSE
;КОН_БЛОК_2
;ИНАЧЕ tf:=FALSE
;КОН_БЛОК_1
@@m4:
mov ax,qq
cmp ax,b ;qq<>b
je @@m9 ;на qq=qq-1
;qq*vn-2>b*rr+uj+n-2
mul v+1 ;qq*vn-2
mov temp,ax ;temp=vn-2*qq
xor ax,ax
mov ax,b
mul rr
xor dx,dx
mov dl,u0[si+2]
add ax,dx
cmp temp,ax ;qq*vn-2 > b*rr+uj+n-2
jna @@m8
@@m9:
dec qq
xor ax,ax
mov al,v
add rr,ax
jmp @@m4
@@m8:
@@m3:
;D4
;//шаг 4 - умножить и вычесть:
;u[j+n…j]:=u[j+n…j]-qq*v[n-1…0]
mul_unsign_NM v,n,qq,1,w
mov bx,si
push si
sub_sign_N u0[bx],w, ;v w
;ЕСЛИ u[j+n…j] 1 ТО
cmp borrow,1 ;был заем на шаге D4 ??
jne @@m6
;НАЧ_БЛОК_4
;//шаг 6 - компенсирующее сложение:
;q[j]:= q[j]-1
;u[j+n…j]:=u[j+n…j]+v[n-1…0]
;КОН_БЛОК_4
;D6 - компенсирующее сложение
mov borrow,0 ;сбросим факт заема
dec q[si]
mov bx,si
push si
add_unsign_N carry,u0[bx],v0, ;перенос не нужен
;D7
;//шаг 7 - цикл по j:
;j:=j-1
@@m6: pop si
inc si
;ЕСЛИ jі0 ТО ПЕРЕЙТИ_НА @@m7
cmp si,mm
jle @@m7
;D8 - денормализация
;//шаг 8 - денормализация:
;//вычислим остаток:
;r[n-1…0]:=u[n-1…0]/d
mov bx,si
div_sign_N u0[bx],N,d,r,temp_r
ret
;//q[m…0] - частное, r[n-1…0] ? астаток
;КОН_ПРОГ
div_unsign_NM endp

main:
mov dx,@data
mov ds,dx

mov ax,4c00h
int 21h
end main

За время программирования чипов AVR, нарыл я разных математических подпрограмм для этих чипов. Может кому пригодятся. Что мне жалко этого добра? Пущай народ чесной пользуется. Если у кого то есть что то еще, то можно добавить это в статью.

Комментарии ( 45 )

Да, это, конечно, круто, но лично я вряд ли возьмусь за ассемблер АВР, хотя если бы были комментарии, кое-что, возможно, использовал бы в пиковском ассемблере, например bin2bcd. Хотя можно и в этом разобраться при желании.

Логическое ударение на ассемблер или на АВР? Конечно лучше бы платформо-независимые алгоритмы, чем конкретная реализация.

Я когда начинал изучать эти чипы, начинал с асма. На самом деле там многие команды дублирующие и вообще никогда ненужны.

Всего 35 команд? Это же ужас как неудобно. Все приходится через задницу делать. Чем больше команд тем проще!

Команд или мнемоник? Тут есть нюанс… У Z80 было под 700 команд. Включая такие которые одной мнемоникой могли целый блок памяти скопировать. Это было круто! У пика же все очень и очень куцо. Даже сложения вроде бы нету.

По моему, таки команд. Я про x86. Если про MSP430 — у него 27 команд и 53 (или около того) мнемоники.

В обоих вариантов есть плюсы и минусы. Для программирования удобнее всего трехадресные системы команд с ортогональной адресацией (когда любой из операндов и результат можно достать/положить любым доступным способом адресации). Эти же наборы команд сложнее всего реализовать в железе и параллельно исполнять. И так же трудно добиться малого потребления. Малое число простых команд, отсутствие заумных методов адресации + (относительно) большой регистровый блок — другая альтернатива. Скорость и малое потребление достигаются относительно легко, малыми затратами железа, но писать становится, мягко говоря, совсем не просто. Неудобство писания на асме решается переходом на более высокоуровневые языки, а вот скорость и потребление так в лоб не решаются. Потому, вобщем, и ушли от сложных систем команд. Впрочем, можно сломать сразу все, в чем легко убедиться посмотрев на х86 — горбатая и при этом сложная система команд, неудобная ни для писания на асме ни для оптимизации ЯВУ. При этом сложная чисто аппаратно и плохо поддающаяся оптимизации под малое потребление.

Это если не знать ничего другого. За сегментную адресацию и привязку некоторых команд к регистрам (которых мало) всегда хотелось стукнуть разработчиков чем-нибудь тяжелым.

Ну почему же ничего другого. Я штук шесть архитектур относительно свободно знаю.

А по х86… не любишь сегменты? Протект мод и флат режим к твоим услугам :) Хотя можно и без ПМ в флэт режиме работать. У Зубкова пример был. Опять же до 64к можно было в COM прекрасно уложиться и в одном сегменте. Вот узкое регистровое горло это да, порой вымораживает. Зато самих комбинаций регистров навалом. Хоть целый, хоть побайтно, в любой комбинации. Куча разных косвенных индексаций, возможность делать паровозы из адресов и смещений. Вообще удобно было с памятью работать, в отличии от той же load-Store. А еще сопроцессор и mmx добавляют лулзов. Хотя я их особо глубоко не копал. Так пару раз для вычисления одной шняги делал, когда писал на асме курсач по МПС (там надо было сэмулировать систему управления из движка, нагрузки и двух обратных связей по моменту и оборотам с ПИД регулятором). Лохов, наш препод по МПС, жог напалмом в заданиях.

Эти все режимы в подметки не годились возможностям PDP-11 или, скажем, Motorola 68000. Замечу, М68К и i86 — одногодки, объяснить кривость x86 тем, что они, типа, первопроходцы, не получается.

Вообще есть расширенный набор команд, используется в пик18 и в новых сериях пик16

В этом разделе мы познакомимся с концепцией раздельной трансляции и научимся собирать программы, состоящие из нескольких модулей.

Немного теории

До сих пор мы имели дело с программами, состоящими из одного модуля. Мы размещали все переменные и весь код в одном файле исходного кода, а затем вызывали ассемблер, чтобы оттранслировать наш модуль (например, hello.asm ) и получить на выходе объектный файл ( hello.obj ):

После этого мы вызывали компоновщик, который собирал из нашего единственного объектного файла — исполняемый файл ( hello.exe ), который можно запустить непосредственно:

Компоновщик может принимать на вход не один, а несколько объектных файлов и компоновать их в один исполняемый файл. Такой подход позволяет разделить программу на отдельные части — модули, каждый из которых отвечает за решение своей частной задачи: например, один модуль может отвечать за ввод-вывод, другой за обработку данных и т. д. При этом один модуль объявляется главным и содержит точку входа в программу, а остальные модули считаются вспомогательными.

Простейшая программа из двух модулей

Начнем с программы, главный модуль которой занимается вводом-выводом, а собственно вычисление вынесено в отдельный (вспомогательный) модуль. Наша программа будет вычислять 25-е число Фиббоначчи.

Модуль main

В главном модуле вызовем процедуру fib25 , описанную во вспомогательном модуле. Процедура fib25 запишет в переменную result (из главного модуля) вычисленное значение (25-е число Фиббоначчи). Затем выведем результат на экран:

Для этого нам нужно не только обычным образом выделить память под переменную result , но и объявить публичные (public) и внешние (external) имена:

Первая строчка сообщает ассемблеру, что имя result нужно сделать публичным, то есть доступным в других модулях, из которых будет скомпонована программа. Вторая строчка говорит, что fib25 — это процедура, которую можно вызвать, но объявлена она (как публичная) будет в некотором другом модуле.

Примечание. Вместо директивы fib25 proto можно использовать директиву extrn fib25@0: near , которая сообщит, что fib25@0 — внешняя метка. В таком случае в инструкции call тоже придется использовать имя fib25@0 . За то, что метка, соответствующая адресу входа в процедуру, получает такое имя, отвечают правила декорирования имен, определяемые действующим соглашением о связях. Подробнее об этом — ниже.

Целиком главный модуль выглядит так:

Модуль fib

Теперь займемся вторым модулем. Начать его нужно с двух директив. Первая директива просит ассемблер генерировать машинный код, совместимый с процессорами не старее Pentium, вторая устанавливает плоскую модель памяти и соглашение о связях stdcall. В наших предыдущих программах мы не указывали эти директивы — они уже есть в файле console.inc , который мы включаем ради макросов ввода-вывода. Однако во вспомогательном модуле, который занимается только вычислениями, ввод-вывод не нужен, поэтому подключать console.inc не требуется:

Далее, объявим внешние и публичные имена. Во вспомогательном модуле мы определим процедуру fib25 , которая будет помещать результат (типа dword ) во внешнюю переменную:

Сама процедура будет крайне простой: она будет вызывать функцию fib (которую мы напишем, опираясь на код из предыдущего раздела), и помещать результат в переменную result :

Функция fib не объявлена как публичная, поэтому вызвать ее из главного модуля напрямую будет невозможно.

Ниже приведен код вспомогательного модуля целиком:

Примечание. Точка входа в программу должна быть только в одном модуле — главном.

Трансляция и компоновка

Теперь вручную соберем и запустим исполняемый файл:

Компоновщик строит исполняемый файл, имя которого совпадает с первым из переданных ему объектных файлов (в нашем случае main ); если требуется иное имя, можно передать параметр /out: , тогда исполняемый файл будет называться .exe .

Эта ошибка происходит именно на стадии компоновки; трансляция же каждого из модулей завершилась успешно.

Автоматизация сборки

Пакетный файл mkr.bat , который мы применяли для трансляции простейших программ, оказывается бесполезным, если программа состоит из нескольких модулей. Удобнее поступить следующим образом: поместить исходный код всех модулей в отдельный каталог (например, нашу программу поместим в каталог fib25 внутри рабочего каталога) и рядом в тот же каталог положить пакетный файл сборки — назовем его build.bat :

Примечание. Размещать каждую программу в отдельном каталоге полезно еще и потому, что в разных программах могут иметься разные модули с одинаковыми именами — например, модуль с именем main .

Для каждой новой многомодульной программы этот файл потребуется модифицировать, добавив по вызову ml для каждого модуля, указав правильный список объектных файлов в вызове link и правильное имя исполняемого файла.

Благодаря конструкции || goto END после каждого из вызовов сборка продолжится только в том случае, если очередной шаг завершился успешно. Если же на каком-то шаге произошла ошибка, то дальнейшие шаги выполняться не будут, и исполняемый файл запускаться тоже не будет:

Примечание. Минусом нашей самодельной системы сборки является то, что сборка выполняется заново полностью, даже если мы изменили только один из модулей. Промышленные системы сборки, которыми обычно пользуются при разработке (make, ant и т. д.) лишены этого недостатка.

Декорирование имен

В самом начале модуля fib мы указали директиву .model flat, stdcall , выбрав тем самым stdcall в качестве соглашения о связях. Помимо всего прочего, stdcall предусматривает определенные правила декорирования имен, то есть правила, по которым из имени процедуры, определенной в модуле, будет сформировано имя для компоновщика. В случае соглашения о связях stdcall к имени добавляется _ в начале и @ плюс размер параметров на стеке в байтах — в конце. У процедуры fib25 нет параметров, поэтому декорированное имя для нее будет выглядеть как _fib25@0 .

Примечание. Соглашение stdcall предполагает, что вызывающий код помещает параметры на стек, а очищает их уже вызываемая процедура. Поэтому если вызывающая и вызываемая стороны не договорились о количестве параметров, то стек будет испорчен. Пример такой ситуации: в модуле А процедура P вызывается, а в модуле B она определена. Изначально она принимает два параметра. Далее в модуле процедура меняется так, чтобы она принимала три параметра. Без декорирования имен сборка по-прежнему будет проходить успешно, но программа перестанет работать, причем катастрофическим образом. Благодаря декорированию же ошибка будет обнаружена на этапе компоновки: модуль A публикует процедуру _P@12 , а модулю B требуется процедура _P@8 .

Чтобы увидеть полную таблицу имен объектного файла, можно воспользоваться программой objdump :

Примечание. У функции fib , если смотреть на ее декорированное имя, нет параметров. На самом деле мы просто не указали их в заголовке функции. Язык ассемблера позволяет это сделать, и об этом мы поговорим в следующем разделе.

Обычная метка, если объявить ее публичной, декорируется проще: к имени добавляется _ в начале. Именно поэтому вместо fib25 proto можно было указать extrn fib25@0: near — в обоих случаях соответствующее декорированное имя получалось _fib25@0 .

Если убрать спецификацию stdcall из директивы .model , то таблица имен для модуля будет выглядеть иначе — имена декорироваться не будут:

Читайте также: