Как работает $ в NASM, точно? объясняет, как $ - msg
заставляет NASM вычислять для вас длину строки как постоянную времени сборки вместо жесткого кодирования.
Первоначально я написал остальную часть этого для SO Docs (topi c ID: 1164, пример ID: 19078) , переписывая основы c менее хорошо прокомментированные пример @ runner. Это выглядит как лучшее место, чтобы поставить его, чем часть моего ответа на другой вопрос , куда я ранее переместил его после окончания эксперимента с SO docs.
Выполнение системного вызова выполняется путем помещения аргументов в регистры, затем запускается int 0x80
(32-разрядный режим) или syscall
(64-разрядный режим). Каковы соглашения о вызовах для системных вызовов UNIX & Linux на i386 и x86-64 и Полное руководство по Linux Системным вызовам .
Думайте о int 0x80
как о способе «вызова» в ядро через границу привилегий пользователя / ядра. Ядро выполняет вещи в соответствии со значениями, которые были в регистрах при выполнении int 0x80
, а затем в конечном итоге возвращается. Возвращаемое значение в EAX.
Когда выполнение достигает точки входа в ядро, оно смотрит на EAX и отправляет нужный системный вызов на основе номера вызова в EAX. Значения из других регистров передаются как аргументы функции в обработчик ядра для этого системного вызова. (например, eax = 4 / int 0x80
заставит ядро вызвать его функцию ядра sys_write
, реализующую системный вызов POSIX write
.)
И также посмотрите Что произойдет, если вы используете 32-битный int 0x80 Linux ABI в 64-битном коде? - этот ответ включает в себя просмотр asm в точке входа ядра, которая "вызывается" int 0x80
. (Также относится к 32-разрядному пользовательскому пространству, а не только к 64-разрядным, где вы не должны использовать int 0x80
).
Если вы еще не знаете низкоуровневые Unix системы При программировании вы можете просто написать функции в asm, которые принимают аргументы и возвращают значение (или обновляют массивы через указатель arg) и вызывают их из программ C или C ++. Тогда вы можете просто позаботиться о том, как обращаться с регистрами и памятью, не изучая API-интерфейс системных вызовов POSIX и ABI для его использования. Это также упрощает сравнение вашего кода с выводом компилятора для реализации C. Компиляторы обычно неплохо справляются с созданием эффективного кода, но редко бывают идеальными .
lib c предоставляет функции-обертки для системных вызовов, поэтому сгенерированный компилятором код будет call write
скорее чем вызывать его напрямую с помощью int 0x80
(или, если вы заботитесь о производительности, sysenter
). (В коде x86-64 используйте syscall
для 64-битного ABI .) См. Также syscalls(2)
.
Системные вызовы описаны в разделе 2. страницы справочника, например write(2)
. В разделе NOTES приведены различия между функцией-оболочкой lib c и системным вызовом Linux. Обратите внимание, что оболочкой для sys_exit
является _exit(2)
, а не функция exit(3)
ISO C, которая сначала очищает буферы stdio и другие операции очистки. Также существует системный вызов exit_group
, который завершает все потоки . exit(3)
фактически использует это, потому что в однопоточном процессе нет недостатков.
Этот код выполняет 2 системных вызова:
Я прокомментировал это (до такой степени, что он начинает скрывать реальный код без цветовой подсветки синтаксиса). Это попытка указать на новичков, а не на то, как вы должны нормально комментировать свой код.
section .text ; Executable code goes in the .text section
global _start ; The linker looks for this symbol to set the process entry point, so execution start here
;;;a name followed by a colon defines a symbol. The global _start directive modifies it so it's a global symbol, not just one that we can CALL or JMP to from inside the asm.
;;; note that _start isn't really a "function". You can't return from it, and the kernel passes argc, argv, and env differently than main() would expect.
_start:
;;; write(1, msg, len);
; Start by moving the arguments into registers, where the kernel will look for them
mov edx,len ; 3rd arg goes in edx: buffer length
mov ecx,msg ; 2nd arg goes in ecx: pointer to the buffer
;Set output to stdout (goes to your terminal, or wherever you redirect or pipe)
mov ebx,1 ; 1st arg goes in ebx: Unix file descriptor. 1 = stdout, which is normally connected to the terminal.
mov eax,4 ; system call number (from SYS_write / __NR_write from unistd_32.h).
int 0x80 ; generate an interrupt, activating the kernel's system-call handling code. 64-bit code uses a different instruction, different registers, and different call numbers.
;; eax = return value, all other registers unchanged.
;;;Second, exit the process. There's nothing to return to, so we can't use a ret instruction (like we could if this was main() or any function with a caller)
;;; If we don't exit, execution continues into whatever bytes are next in the memory page,
;;; typically leading to a segmentation fault because the padding 00 00 decodes to add [eax],al.
;;; _exit(0);
xor ebx,ebx ; first arg = exit status = 0. (will be truncated to 8 bits). Zeroing registers is a special case on x86, and mov ebx,0 would be less efficient.
;; leaving out the zeroing of ebx would mean we exit(1), i.e. with an error status, since ebx still holds 1 from earlier.
mov eax,1 ; put __NR_exit into eax
int 0x80 ;Execute the Linux function
section .rodata ; Section for read-only constants
;; msg is a label, and in this context doesn't need to be msg:. It could be on a separate line.
;; db = Data Bytes: assemble some literal bytes into the output file.
msg db 'Hello, world!',0xa ; ASCII string constant plus a newline (0x10)
;; No terminating zero byte is needed, because we're using write(), which takes a buffer + length instead of an implicit-length string.
;; To make this a C string that we could pass to puts or strlen, we'd need a terminating 0 byte. (e.g. "...", 0x10, 0)
len equ $ - msg ; Define an assemble-time constant (not stored by itself in the output file, but will appear as an immediate operand in insns that use it)
; Calculate len = string length. subtract the address of the start
; of the string from the current position ($)
;; equivalently, we could have put a str_end: label after the string and done len equ str_end - str
Обратите внимание, что мы не храним длину строки в памяти данных где-либо , Это постоянная времени сборки, поэтому эффективнее иметь ее в качестве непосредственного операнда, чем нагрузки. Мы могли бы также поместить строковые данные в стек с помощью трех push imm32
инструкций, но слишком большое увеличение размера кода не очень хорошая вещь.
На Linux вы можете сохранить этот файл как Hello.asm
и собрать из него 32-битный исполняемый файл с помощью этих команд :
nasm -felf32 Hello.asm # assemble as 32-bit code. Add -Worphan-labels -g -Fdwarf for debug symbols and warnings
gcc -static -nostdlib -m32 Hello.o -o Hello # link without CRT startup code or libc, making a static binary
См. this Ответьте для получения более подробной информации о сборке сборки в 32- или 64-битные исполняемые файлы * * * * * * * * * * * или * * * * * * * 11 для динамического связывания, для синтаксиса NASM / YASM или синтаксиса GNU AT & T с директивами GNU as
. (Ключевой момент: обязательно используйте -m32
или эквивалентный при построении 32-битного кода на 64-битном хосте, иначе у вас будут запутанные проблемы во время выполнения.)
Вы можете отследить его выполнение с помощью strace
, чтобы увидеть системные вызовы, которые он делает :
$ strace ./Hello
execve("./Hello", ["./Hello"], [/* 72 vars */]) = 0
[ Process PID=4019 runs in 32 bit mode. ]
write(1, "Hello, world!\n", 14Hello, world!
) = 14
_exit(0) = ?
+++ exited with 0 +++
Сравните это с трассировкой для динамически связанного процесса (например, g cc делает из hello. c, или от запуска strace /bin/ls
), чтобы понять, сколько всего происходит под капотом для динамического c связывания и C запуска библиотеки.
Трассировка на stderr и регулярный вывод на В этом случае оба stdout собираются в терминал, поэтому они взаимодействуют в линии с системным вызовом write
. Перенаправьте или проследите к файлу, если вы заботитесь. Обратите внимание, что это позволяет нам легко видеть возвращаемые значения системного вызова без необходимости добавлять код для их печати, и на самом деле это даже проще, чем использовать обычный отладчик (например, gdb) для одношагового выполнения и посмотреть на eax
. В нижней части x86 wiki приведены советы по gdb asm. (Остальная часть тега wiki полна ссылок на хорошие ресурсы.)
Версия этой программы x86-64 будет чрезвычайно похожа, передавая одни и те же аргументы одним и тем же системным вызовам, только в разных регистрах и с syscall
вместо int 0x80
. См. Нижнюю часть Что произойдет, если вы используете 32-битный int 0x80 Linux ABI в 64-битном коде? для рабочего примера написания строки и выхода в 64-битном коде.
related: Учебное пособие по созданию действительно исполняемых файлов ELF для Linux. Самый маленький двоичный файл, который вы можете запустить, просто выполняет системный вызов exit (). Речь идет о минимизации двоичного размера, а не размера исходного кода или даже количества фактически выполняемых инструкций.