Какие части этого ассемблерного кода HelloWorld необходимы, если я пишу программу в ассемблере?



У меня есть такая короткая программа hello world:



#include <stdio.h>

static const char* msg = "Hello world";

int main(){
printf("%sn", msg);
return 0;
}


Я скомпилировал его в следующий ассемблерный код с помощью gcc:



    .file   "hello_world.c"
.section .rodata
.LC0:
.string "Hello world"
.data
.align 4
.type msg, @object
.size msg, 4
msg:
.long .LC0
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl msg, %eax
movl %eax, (%esp)
call puts
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits


Мой вопрос: Являются ли все части этого кода существенными, если бы я написал эту программу в сборке (вместо того, чтобы писать ее в C и затем компилировать в сборку)? Я понимаю инструкции по сборке, но есть некоторые части, которые я не понимаю. Например, я не знаю, что именно .cfi * есть, и мне интересно, нужно ли мне включить это, чтобы написать это программа в сборке.

654   2  

2 ответов:

Абсолютный минимум, который будет работать на платформе, которой это кажется, является

        .globl main
main:
        pushl   $.LC0
        call    puts
        addl    $4, %esp
        xorl    %eax, %eax
        ret
.LC0:
        .string "Hello world"
Но это нарушает ряд требованийABI . Минимум для ABI-совместимой программы равен
        .globl  main
        .type   main, @function
main:
        subl    $24, %esp
        pushl   $.LC0
        call    puts
        xorl    %eax, %eax
        addl    $28, %esp
        ret
        .size main, .-main
        .section .rodata
.LC0:
        .string "Hello world"
Все остальное в вашем объектном файле - это либо компилятор, который не оптимизирует код настолько сильно, насколько это возможно, либо необязательные аннотации, которые будут записаны в объектный файл.

Директивы .cfi_*, в частности, являются необязательными аннотациями. Они являются Необходимо тогда и только тогда, когда функция может находиться в стеке вызовов при возникновении исключения C++, но они полезны в любой программе, из которой вы можете извлечь трассировку стека. Если вы собираетесь писать нетривиальный код вручную на ассемблере, то, вероятно, стоит научиться их писать. К сожалению, они очень плохо документированы; в настоящее время я не нахожу ничего, что, по моему мнению, стоит связывать.

Линия

.section    .note.GNU-stack,"",@progbits

Является также важно знать, пишете ли вы язык ассемблера вручную; это еще одна необязательная аннотация, но ценная, потому что она означает: "ничто в этом объектном файле не требует, чтобы стек был исполняемым."Если все объектные файлы в программе имеют эту аннотацию, ядро не сделает стек исполняемым, что немного повышает безопасность.

(чтобы указать, что вам действительно нужен исполняемый стек, вы ставите "x" вместо "". GCC может сделать это, если вы используете его расширение "вложенная функция". (Не делай этого.))

Вероятно, стоит упомянуть, что в синтаксисе сборки "AT&T", используемом (по умолчанию) GCC и GNU binutils, есть три вида строк: с единственным маркером на нем, заканчивающимся двоеточием, это ярлык. (Я не помню правил, по которым символы могут появляться в ярлыках.) Строка, чейПервый токен начинается с точки и незаканчивается двоеточием, является своего рода директивой для ассемблера. Все остальное-это инструкция по сборке.

Связанные вопросы: Как удалить " шум " с выхода сборки GCC/clang? директивы .cfi не являются непосредственно полезными для вас, и программа будет работать без них. (Это информация о развертывании стека, необходимая для обработки исключений и обратных следов, поэтому -fomit-frame-pointer может быть включена по умолчанию. И да, gcc излучает это даже для C.)


Что касается количества исходных строк asm, необходимых для создания программы value Hello World, очевидно, что мы хотим использовать функции libc для выполнения большей работы для нас.

Ответ@Zwol имеет самую короткую реализацию вашего исходного кода C.

Вот что вы можете сделать вручную , если вас не волнует статус выхода вашей программы, просто то, что она печатает вашу строку.

    .globl main
main:
    # main gets two args: argv and argc, so we know we can modify the 8 bytes above our return address.
    mov     $.LC0, 4(%esp)     # replace our first arg with the string
    jmp     puts               # tail-call puts.

# you would normally put the string in .rodata, not leave it in .text where the linker will mix it with other functions.
.LC0:
    .asciz "Hello world"     # asciz zero-terminates

Эквивалентный C (вы только что попросили самый короткий Hello World, а не тот, который имел идентичную семантику):

int main(int argc, char **argv) {
    return puts("Hello world");
}

Его статус выхода не определен, но он определенно печатает. puts(3) возвращает " неотрицательное число", что может быть за пределами 0..Диапазон 255, поэтому мы ничего не можем сказать о состоянии выхода программы, равном 0 / ненулевому в Linux (где состояние выхода процесса-это младшие 8 бит целого числа, переданного системному вызову exit_group() (в данном случае кодом запуска CRT, который вызвал main ())).


Использование JMP для реализации хвостового вызова является стандартной практикой и обычно используется, когда функция не должна ничего делать после того, как другая функция возвращает. puts() будет в конечном итоге вернитесь к функции, которая вызвала main(), точно так же, как если бы puts() вернулся в main (), а затем main() вернулся. вызывающий main () все еще должен иметь дело с ARG, которые он поместил в стек для main (), потому что они все еще там (но модифицированы, и нам разрешено это делать).

Gcc и clang не генерируют код, который изменяет пространство ARG-передачи в стеке. Однако это совершенно безопасно и ABI-совместимо: функции "владеют" своими ARG в стеке, даже если они были const. Если вы называете функция, вы не можете предположить, что args, которые вы положили в стек, все еще там. Чтобы сделать еще один вызов с теми же или подобными args, вам нужно сохранить их все снова.

Также обратите внимание, что это вызывает puts() с тем же выравниванием стека, которое у нас было при входе в main(), поэтому мы снова ABI-совместимы в сохранении выравнивания 16B, требуемого современной версией x86-32 aka i386 System V ABI (используемой Linux).

.string нуль-завершает строки, как и .asciz, но мне пришлось посмотрите его, чтобы проверить . Я бы рекомендовал просто использовать .ascii или .asciz, чтобы убедиться, что вам ясно, есть ли в ваших данных завершающий байт или нет. (Он вам не понадобится, если вы используете его с явными функциями длины, такими как write())


В системе x86-64 V ABI (и Windows) args передаются в регистрах. Это делает оптимизацию хвостового вызова намного проще, потому что вы можете переставлять args или передавать больше args (до тех пор, пока у вас не закончатся регистры). Этот заставляет компиляторов делать это на практике. (Потому что, как я уже сказал, они в настоящее время не генерируют код, который изменяет входящее пространство arg в стеке, хотя ABI ясно, что им это разрешено, и функции, созданные компилятором, предполагают, что функции разбивают их ARG стека.)

Clang или gcc-O3 сделают эту оптимизацию для x86-64, , как вы можете видеть на проводнике компилятора Godbolt:

#include <stdio.h>
int main() { return puts("Hello World"); }

# clang -O3 output
main:                               # @main
    movl    $.L.str, %edi
    jmp     puts                    # TAILCALL

 # Godbolt strips out comment-only lines and directives; there's actually a .section .rodata before this
.L.str:
    .asciz  "Hello World"

Статические адреса данных всегда помещаются в нижнюю 31 биты адресного пространства и исполняемый файл не нуждаются в позиционно-независимом коде, иначе mov был бы lea .LC0(%rip), %rdi. (Вы получите это от gcc, если он был настроен с помощью --enable-default-pie чтобы сделать исполняемые файлы независимыми от позиции.)


Hello World использует 32-битные системные вызовы x86 Linux напрямую, без libc

я первоначально написал это для так, Документы (идентификатор раздела: 1164, пример ID: 19078), переписывая основную меньше-хорошо прокомментированный пример с @бегунка. это в Синтаксис NASM, поэтому он не идеально подходит для этого вопроса.


Если вы еще не знаете низкоуровневое программирование систем Unix, вы можете просто написать функции в asm, которые принимают args и возвращают значение (или обновляют массивы через указатель arg) и вызывают их из программ C или C++. Тогда вы можете просто беспокоиться о том, чтобы научиться обрабатывать регистры и память, не изучая также POSIX system-call API и ABI для его использования. Это также упрощает сравнение вашего кода с выводом компилятора для реализации на языке Си. Компиляторы обычно делают довольно хорошую работу по созданию эффективного кода, но редко совершенны.

Libc предоставляет функции-оболочки для системных вызовов, поэтому компилятор генерирует код call write, а не вызывает его напрямую с помощью int 0x80 (или, если вы заботитесь о производительности, sysenter). (В коде x86-64 используется syscall для 64-разрядного ABI.) См. также syscalls(2).

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

Смотрите этот ответ для получения более подробной информации о сборке в 32 или 64-битные статические или динамически связанные исполняемые файлы Linux, для синтаксиса 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 +++

Сравните это с трассировкой для динамически связанного процесса (например, gcc делает из hello.c, или от запуска strace /bin/ls), чтобы получить представление о том, сколько всего происходит под капотом для динамическое связывание и запуск библиотеки Си.

Трассировка на stderr и обычный вывод на stdout оба идут к терминалу здесь, поэтому они вмешиваются в линию с системным вызовом write. Перенаправление или трассировка в файл, если вам не все равно. Обратите внимание, как это позволяет нам легко видеть возвращаемые значения syscall без необходимости добавлять код для их печати, и на самом деле даже проще, чем использовать обычный отладчик (например, gdb) для одноступенчатого просмотра eax для этого. Смотрите нижнюю часть тега x86 wiki для GDB ASM tips. (Остальная часть Вики-тега полна ссылок на хорошие ресурсы.)

Версия этой программы на x86-64 была бы чрезвычайно похожа, передавая одни и те же args одним и тем же системным вызовам, только в разных регистрах и с syscall вместо int 0x80. Смотрите нижнюю часть что произойдет, если вы используете 32-разрядный int 0x80 Linux ABI в 64-разрядном коде? для рабочего примера записи строки и выхода из нее в 64-битном коде.

Связанные: A Вихрь учебник по созданию действительно крошечных исполняемых файлов ELF для Linux . Самый маленький двоичный файл, который вы можете запустить, просто делает системный вызов exit (). Речь идет о минимизации двоичного размера, а не исходного размера или даже просто количества инструкций, которые фактически выполняются.

Comments

    Ничего не найдено.