Hello, world!
From Wiki.conus.info
Начнем с знаменитого примера из книги The C Programming Language:
#include <stdio.h> int main() { printf("hello, world"); return 0; };
Компилируем в MSVC 2010: cl 1.cpp /Fa1.asm
(Ключ /Fa означает сгенерировать листинг на ассемблере)
CONST SEGMENT $SG3830 DB 'hello, world', 00H CONST ENDS PUBLIC _main EXTRN _printf:PROC ; Function compile flags: /Odtp _TEXT SEGMENT _main PROC ; File c:\...\1.cpp ; Line 4 push ebp mov ebp, esp ; Line 5 push OFFSET $SG3830 call _printf add esp, 4 ; Line 6 xor eax, eax ; Line 7 pop ebp ret 0 _main ENDP _TEXT ENDS
Компилятор сгенерировал файл 1.obj, который впоследствии будет слинкован линкером в 1.exe. Здесь файл состоит из двух сегментов: CONST (для данных-констант) и _TEXT (для кода). Строка "hello, world" в Си имеет тип const char*, однако не имеет имени. Компилятору нужно как-то с ней работать, так что он дает ей внутреннее имя $SG3830. Как видно, строка заканчивается нулевым байтом - это требования стандарта Си насчет строк. В сегменте кода _TEXT находится пока только одна функция - _main.
Функция _main, как и практически все функции, начинается с пролога и заканчивается эпилогом. Об этом смотрите подробнее тут: пролог и эпилог в функции.
Далее следует вызов функции _printf(): CALL _printf. Перед этим вызовом, адрес строки (или указатель на нее) с нашим приветствием при помощи инструкции PUSH помещается в стек. После того как функция _printf() возвращает управление, адрес строки все еще лежит в стеке. Так как он больше не нужен, то указатель стека (регистр ESP) корректируется. "ADD ESP, 4" означает прибавить 4 к значению в регистре ESP. Так как, это 32-битный код, для передачи адреса нужно аккурат 4 байта. Некоторые компиляторы, например Intel C++ Compiler, в этой же ситуации, могут вместо ADD сгенерировать "POP ECX" (это можно встретить в коде Oracle RDBMS, им скомпилированном), что почти то же самое, только портится значение в регистре ECX. Возможно, компилятор применяет POP ECX потому что эта инструкция короче.
Немного о стеке: стек.
После вызова printf, в оригинальном коде на Си было "return 0" - вернуть 0 в качестве результата. В сгенерированном коде это обеспечивается инструкцией "XOR EAX, EAX". XOR, на самом деле, как нетрудно догадаться, "исключающее ИЛИ", но компиляторы часто используют его вместо простого "MOV EAX, 0" - опкод немного короче. Бывает так, что некоторые компиляторы генерируют "SUB EAX, EAX", что значит, отнять значение EAX от EAX, в любом случае это даст 0 в результате.
Самая последняя инструкция RET возвращает управление в вызывающую функцию.
Теперь скомпилируем то же самое компилером GCC 4.4.1 в Linux: gcc 1.c -o 1
Затем при помощи IDA посмотрим как создалась функция _main. С другой стороны, мы можем посмотреть результат работы GCC при помощи ключа -S -masm=intel.
main proc near var_10 = dword ptr -10h push ebp mov ebp, esp and esp, 0FFFFFFF0h sub esp, 10h mov eax, offset aHelloWorld ; "hello, world" mov [esp+10h+var_10], eax call _printf mov eax, 0 leave retn main endp
Почти то же самое, за исключением того что в прологе функции мы видим AND ESP, 0FFFFFFF0h - эта инструкция выравнивает значение в ESP по 16-байтной границе, делая некоторые значения в стеке также выровненными по этой границе. SUB ESP, 10h выделяет в стеке 16 байт, хотя, как будет видно далее, нам достаточно только 4. Это происходит потому что количество выделяемого места в локальном стеке тоже выровнено по 16-байтной границе.
Указатель на строку затем записывается прямо в стек без помощи инструкции PUSH. Затем вызывается printf. В отличие от MSVC, GCC в компиляции без оптимизации генерит MOV EAX, 0 вместо более короткого опкода.
Последняя инструкция LEAVE - это аналог команд MOV ESP, EBP и POP EBP - то есть возврат указателя стека и регистра EBP в первоначальное состояние. Это необходимо, т.к., в начале функции мы модифицировали его (при помощи AND ESP, ...).