Обычно код программы состоит не из одного файла, а из нескольких файлов. Разбиение программы на отдельные файлы позволяет упростить работу над логикой приложения. Функционал из одной файла, например, какие-то отдельные функции, можно легко переностить и повторно использовать в других программах. В связи с этим возникает вопрос организации программы. И тут у нас есть различные варианты.
Пусть у нас есть следующий файл sum.asm с кодом функции, которая вычисляет сумму чисел:
section .text ; Функция возвращает сумму чисел ; Принимает два параметра: ; rdi - первое число ; rsi - второе число ; Результат функции возвращается через регистр rax sum: mov rax, rdi ; результат в rax add rax, rsi ret
Через регистры rdi и rsi функция принимает два числа и через регистр rax возвращает их сумму.
За один раз NASM (по крайней мере на момент написания данной статьи) может компилировать только один файл с исходным кодом. Как быть, если у нас несколько файлов? Первый вариант - использовать директиву %INCLUDE, которая вставляет код одной файла в определенное место другого файла.
Так, пусть главный файл программы (пример на Linux) называется hello.asm, и он использует выше определенную функцию:
global _start section .text %INCLUDE "sum.asm" _start: mov rdi, 33 mov rsi, 44 call sum mov rdi, rax ; для проверки результата помещаем сумму из RAX в RDI mov rax, 60 syscall
Здесь выражение %INCLUDE "sum.asm"
будет вставлять на место данной директивы код файла "sum.asm", который как предполагается, располагается в той же папке.
Компиляция в этом случае происходит как и в общем случае (команда компиляции для Linux):
nasm -f elf64 hello.asm -o hello.o
Аналогичный пример для Windows:
global _start section .text %INCLUDE "sum.asm" _start: mov rdi, 33 mov rsi, 44 call sum ret
Второй подход представляет раздельную компиляцию файлов в разные объектные файлы, а при компоновке они объединяются в один исполняемый файл. Но в этом случае те метки (функции, данные), которые мы хотим сделать видимыми извне и которые должны использоваться во внешнем коде, объявляются с помощью директивы global. Так, определим следующий файл sum.asm:
global sum ; делаем функцию sum доступной извне section .text ; Функция возвращает сумму чисел ; Принимает два параметра: ; rdi - первое число ; rsi - второе число ; Результат функции возвращается через регистр rax sum: mov rax, rdi ; результат в rax add rax, rsi ret
Вначале идет директива global, которая делает видимой извне или иначе говоря глобальной функцию sum:
global sum
В файле кода может быть определено множество функций, переменных, констант, но вне файла будут доступны только те из них, которые объявлены с помощью директивы global. Причем подобное объявление с global должно идти до определения функций/переменных/констант.
Чтобы использовать подобные глобальные функции/переменные/константы в другом файлы они должны быть объявлены с помощью директивы extern. Например, используем функцию sum в файле hello.asm:
global _start extern sum ; функция sum расположена где-то во вне section .text _start: mov rdi, 33 mov rsi, 44 call sum mov rdi, rax ; для проверки результата помещаем сумму из RAX в RDI mov rax, 60 syscall
Выражение extern sum
указывает, что функция sum расположена где-то во вне, а где именно, станет известным на стадии компоновки. Ассемблер NASM увидит это, и не будет
выдывать никаких ошибок на стадии компиляции.
Теперь соберем программу на примере Linux. Сначала скомпилируем файл sum.asm:
nasm -f elf64 sum.asm -o sum.o
Затем скомпилируем файл hello.asm:
nasm -f elf64 hello.asm -o hello.o
В итоге у нас получится два разных объектных файла - sum.o и hello.o. Скомпонуем их в один исполняемый файл:
ld hello.o sum.o -o hello
Здесь стандартному компоновщику в Linux - программе ld передаются оба объектных файла, и на выходе мы получаем исполняемый файл "hello", который мы можем запускать. Полный пример с компиляцией/компоновкой и выполнением программы:
root@Eugene:~/asm# nasm -f elf64 sum.asm -o sum.o root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# ld hello.o sum.o -o hello root@Eugene:~/asm# ./hello root@Eugene:~/asm# echo $? 77 root@Eugene:~/asm#
На Windows все делается аналогично - сначала создаются два объектных файла, которые затем передаются компоновщику для компоновки в исполняемый файл.