Привет, мир на ассемблере с Linux системными вызовами? - PullRequest
0 голосов
/ 30 апреля 2020
  1. Я знаю, что int 0x80 делает прерывание в linux. Но я не понимаю, как работает этот код. Это что-то возвращает?

  2. Что означает $ - msg? 1009 *

global _start

section .data
    msg db "Hello, world!", 0x0a
    len equ $ - msg

section .text
_start:
    mov eax, 4
    mov ebx, 1
    mov ecx, msg
    mov edx, len
    int 0x80 ;What is this?
    mov eax, 1
    mov ebx, 0
    int 0x80 ;and what is this?

1 Ответ

1 голос
/ 30 апреля 2020

Как работает $ в 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 (). Речь идет о минимизации двоичного размера, а не размера исходного кода или даже количества фактически выполняемых инструкций.

...