Многомодульные программы
В этом разделе мы познакомимся с концепцией раздельной трансляции и научимся собирать программы, состоящие из нескольких модулей.
Немного теории
До сих пор мы имели дело с программами, состоящими из одного модуля. Мы
размещали все переменные и весь код в одном файле исходного кода, а затем
вызывали ассемблер, чтобы оттранслировать наш модуль (например, hello.asm
)
и получить на выходе объектный файл (hello.obj
):
C:\work>ml /c /coff hello.asm
После этого мы вызывали компоновщик, который собирал из нашего единственного
объектного файла — исполняемый файл (hello.exe
), который можно
запустить непосредственно:
C:\work>link /subsystem:console hello.obj
Компоновщик может принимать на вход не один, а несколько объектных файлов и компоновать их в один исполняемый файл. Такой подход позволяет разделить программу на отдельные части — модули, каждый из которых отвечает за решение своей частной задачи: например, один модуль может отвечать за ввод-вывод, другой за обработку данных и т. д. При этом один модуль объявляется главным и содержит точку входа в программу, а остальные модули считаются вспомогательными.
Простейшая программа из двух модулей
Начнем с программы, главный модуль которой занимается вводом-выводом, а собственно вычисление вынесено в отдельный (вспомогательный) модуль. Наша программа будет вычислять 25-е число Фиббоначчи.
Модуль main
В главном модуле вызовем процедуру fib25
, описанную во вспомогательном
модуле. Процедура fib25
запишет в переменную result
(из главного модуля)
вычисленное значение (25-е число Фиббоначчи). Затем выведем результат на
экран:
call fib25
outwordln result
Для этого нам нужно не только обычным образом выделить память под переменную
result
, но и объявить публичные (public) и внешние (external) имена:
public result
fib25 proto
; or
; extrn fib25@0: near
Первая строчка сообщает ассемблеру, что имя result
нужно сделать
публичным, то есть доступным в других модулях, из которых будет скомпонована
программа. Вторая строчка говорит, что fib25
— это процедура, которую
можно вызвать, но объявлена она (как публичная) будет в некотором другом
модуле.
Примечание. Вместо директивы fib25 proto
можно использовать директиву
extrn fib25@0: near
, которая сообщит, что fib25@0
— внешняя метка.
В таком случае в инструкции call
тоже придется использовать имя fib25@0
.
За то, что метка, соответствующая адресу входа в процедуру, получает такое
имя, отвечают правила декорирования имен, определяемые действующим
соглашением о связях. Подробнее об этом — ниже.
Целиком главный модуль выглядит так:
; main.asm
include console.inc
public result
fib25 proto
; or
; extrn fib25@0: near
.data
result dd ?
.code
Start:
call fib25
; or
; call fib25@0
outwordln result
exit
end Start
Модуль fib
Теперь займемся вторым модулем. Начать его нужно с двух директив. Первая
директива просит ассемблер генерировать машинный код, совместимый с
процессорами не старее Pentium, вторая устанавливает плоскую модель памяти и
соглашение о связях stdcall. В наших предыдущих программах мы не указывали
эти директивы — они уже есть в файле console.inc
, который мы включаем
ради макросов ввода-вывода. Однако во вспомогательном модуле, который
занимается только вычислениями, ввод-вывод не нужен, поэтому подключать
console.inc
не требуется:
.586
.model flat, stdcall
Далее, объявим внешние и публичные имена. Во вспомогательном модуле мы
определим процедуру fib25
, которая будет помещать результат (типа dword
)
во внешнюю переменную:
public fib25
extrn result: dword
Сама процедура будет крайне простой: она будет вызывать функцию fib
(которую мы напишем, опираясь на код из предыдущего
раздела), и помещать результат в переменную result
:
push 25
call fib
mov result, eax
Функция fib
не объявлена как публичная, поэтому вызвать ее из главного
модуля напрямую будет невозможно.
Ниже приведен код вспомогательного модуля целиком:
; fib.asm
.586
.model flat, stdcall
public fib25
extrn result: dword
.code
; procedure fib25()
; Calculate F_25, put result into the external variable `result`.
fib25 proc
push ebp
mov ebp, esp
push eax
push 25
call fib
mov result, eax
pop eax
mov esp, ebp
pop ebp
ret
fib25 endp
; function fib(n: dword): dword
; Calculate and return F_n. If overflows, return -1.
fib proc
push ebp
mov ebp, esp
push ebx
push ecx
push edx
mov edx, [ebp+8] ; n
mov eax, 1 ; F_2=F_k
mov ebx, eax ; F_1
mov ecx, 2 ; k=2
cmp edx, 2 ; F_1 or F_2? we already have the answer
jbe @ret
@nx: xchg ebx, eax ; ebx <- F_k
add eax, ebx ; eax <- F_k+1
jc @ofl
inc ecx ; k = k + 1
cmp ecx, edx
jb @nx
jmp @ret
@ofl: mov eax, -1 ; return -1 on overflow
@ret: pop ebx
pop ecx
pop edx
mov esp, ebp
pop ebp
ret 4
fib endp
end
Примечание. Точка входа в программу должна быть только в одном модуле — главном.
Трансляция и компоновка
Теперь вручную соберем и запустим исполняемый файл:
C:\work\fib25>ml /nologo /c /coff /Fl fib.asm
Assembling: fib.asm
C:\work\fib25>ml /nologo /c /coff /Fl main.asm
Assembling: main.asm
C:\work\fib25>link /nologo /subsystem:console main.obj fib.obj
C:\work\fib25>main
75025
Компоновщик строит исполняемый файл, имя которого совпадает с первым из
переданных ему объектных файлов (в нашем случае main
); если требуется иное
имя, можно передать параметр /out:<name>
, тогда исполняемый файл будет
называться <name>.exe
.
Во время компоновки внешние и публичные имена различных модулей должны
«состыковаться»: для каждого внешнего имени в одном из модулей должно
найтись соответствующее публичное. Если это не так, мы получим ошибку
unresolved external. Например, если не объявить переменную result
как
публичную, то компоновщик выдаст вот такую диагностику:
C:\work\fib25>link /nologo /subsystem:console main.obj fib.obj
fib.obj : error LNK2001: unresolved external symbol _result
main.exe : fatal error LNK1120: 1 unresolved externals
Эта ошибка происходит именно на стадии компоновки; трансляция же каждого из модулей завершилась успешно.
Автоматизация сборки
Пакетный файл mkr.bat
, который мы применяли для трансляции простейших
программ, оказывается бесполезным, если программа состоит из нескольких
модулей. Удобнее поступить следующим образом: поместить исходный код всех
модулей в отдельный каталог (например, нашу программу поместим в каталог
fib25
внутри рабочего каталога) и рядом в тот же каталог положить пакетный
файл сборки — назовем его build.bat
:
@echo off
ml /nologo /c /coff /Fl main.asm || goto END
ml /nologo /c /coff /Fl fib.asm || goto END
echo Linking
link /nologo /subsystem:console main.obj fib.obj || goto END
echo Running main.exe
echo ------------------
main
:END
Примечание. Размещать каждую программу в отдельном каталоге полезно еще
и потому, что в разных программах могут иметься разные модули с одинаковыми
именами — например, модуль с именем main
.
Для каждой новой многомодульной программы этот файл потребуется
модифицировать, добавив по вызову ml
для каждого модуля, указав правильный
список объектных файлов в вызове link
и правильное имя исполняемого файла.
C:\work\fib25>build
Assembling: main.asm
Assembling: fib.asm
Linking
Running main.exe
------------------
75025
Благодаря конструкции || goto END
после каждого из вызовов сборка
продолжится только в том случае, если очередной шаг завершился успешно. Если
же на каком-то шаге произошла ошибка, то дальнейшие шаги выполняться не
будут, и исполняемый файл запускаться тоже не будет:
C:\work\fib25>build
Assembling: main.asm
Assembling: fib.asm
fib.asm(14) : error A2022: instruction operands must be the same size
Примечание. Минусом нашей самодельной системы сборки является то, что сборка выполняется заново полностью, даже если мы изменили только один из модулей. Промышленные системы сборки, которыми обычно пользуются при разработке (make, ant и т. д.) лишены этого недостатка.
Декорирование имен
В самом начале модуля fib
мы указали директиву .model flat, stdcall
,
выбрав тем самым stdcall в качестве соглашения о связях. Помимо всего
прочего, stdcall предусматривает определенные правила декорирования имен,
то есть правила, по которым из имени процедуры, определенной в модуле, будет
сформировано имя для компоновщика. В случае соглашения о связях stdcall к
имени добавляется _
в начале и @
плюс размер параметров на стеке в
байтах — в конце. У процедуры fib25
нет параметров, поэтому
декорированное имя для нее будет выглядеть как _fib25@0
.
Примечание. Соглашение stdcall предполагает, что вызывающий код помещает
параметры на стек, а очищает их уже вызываемая процедура. Поэтому если
вызывающая и вызываемая стороны не договорились о количестве параметров, то
стек будет испорчен. Пример такой ситуации: в модуле А процедура P
вызывается, а в модуле B она определена. Изначально она принимает два
параметра. Далее в модуле процедура меняется так, чтобы она принимала три
параметра. Без декорирования имен сборка по-прежнему будет проходить
успешно, но программа перестанет работать, причем катастрофическим образом.
Благодаря декорированию же ошибка будет обнаружена на этапе компоновки:
модуль A публикует процедуру _P@12
, а модулю B требуется процедура _P@8
.
Чтобы увидеть полную таблицу имен объектного файла, можно воспользоваться
программой objdump
:
C:\work\mma\1fib>objdump -t fib.obj
fib.obj: file format pe-i386
SYMBOL TABLE:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x00000000 fib.asm
File
[ 2](sec -1)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x001220fc @comp.id
[ 3](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .text
AUX scnlen 0x49 nreloc 1 nlnno 0
[ 5](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .data
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 7](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 _result
[ 8](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000 _fib25@0
[ 9](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000015 _fib@0
Примечание. У функции fib
, если смотреть на ее декорированное имя, нет
параметров. На самом деле мы просто не указали их в заголовке функции. Язык
ассемблера позволяет это сделать, и об этом мы поговорим в следующем разделе.
Обычная метка, если объявить ее публичной, декорируется проще: к имени
добавляется _
в начале. Именно поэтому вместо fib25 proto
можно было
указать extrn fib25@0: near
— в обоих случаях соответствующее
декорированное имя получалось _fib25@0
.
Если убрать спецификацию stdcall
из директивы .model
, то таблица имен
для модуля будет выглядеть иначе — имена декорироваться не будут:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x00000000 fib.asm
File
[ 2](sec -1)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x001220fc @comp.id
[ 3](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .text
AUX scnlen 0x49 nreloc 1 nlnno 0
[ 5](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .data
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 7](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 result
[ 8](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000 fib25
[ 9](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000015 fib