Выравнивание 16-байтового стека
Одной из серьезных проблем с вашим кодом является выравнивание стека. 32-битный код OS/X требует выравнивания стека по 16 байтам в момент выполнения CALL. Соглашение о вызовах Apple IA-32 говорит следующее:
Соглашения о вызовах функций, используемые в среде IA-32, такие же, как и в Система V IA-32 ABI со следующими исключениями:
- Различные правила возврата структур
- Стек выравнивается по 16 байтам в точке вызова функции
- Большие типы данных (более 4 байтов) сохраняются в своем естественном выравнивании.
- Большинство операций с плавающей запятой выполняются с использованием модуля SSE вместо x87 FPU, за исключением случаев, когда используются длинные двойные значения. (Среда IA-32 по умолчанию использует 64-битную внутреннюю точность для x87 FPU.)
Вы вычитаете 12 из ESP, чтобы выровнять стек по 16-байтовой границе (4 байта для адреса возврата + 12 = 16). Проблема в том, что когда вы выполняете CALL функцию, стек ДОЛЖЕН быть выровнен по 16 байтам непосредственно перед самим CALL. К сожалению, вы вставляете 4 байта перед вызовом printf
и exit
. Это смещает стек на 4, тогда как он должен быть выровнен по 16 байтам. Вам придется переработать код с правильным выравниванием. Кроме того, вы должны очистить стек после того, как сделаете вызов. Если вы используете PUSH для помещения параметров в стек, вам нужно настроить ESP после вашего CALL, чтобы восстановить стек в его предыдущее состояние.
Одним из наивных способов (не моя рекомендация) исправить код было бы сделать это:
section .data
hello db "Hello, world", 0x0a, 0x00
section .text
default rel
global _main
extern _printf, _exit
_main:
sub esp, 8
push hello ; 4(return address)+ 8 + 4 = 16 bytes stack aligned
call _printf
add esp, 4 ; Remove arguments
push 0 ; 4 + 8 + 4 = 16 byte alignment again
call _exit ; This will not return so no need to remove parameters after
Приведенный выше код работает, потому что мы можем воспользоваться тем фактом, что обе функции (exit
и printf
) требуют размещения в стеке ровно одного DWORD для параметров. 4 байта для адреса возврата main
, 8 для корректировки стека, которую мы сделали, 4 для параметра DWORD = выравнивание по 16 байтам.
Лучший способ сделать это — вычислить объем стека, который вам потребуется для всех ваших локальных переменных на основе стека (в данном случае 0) в вашей функции main
, плюс максимальное количество байтов, которое вам потребуется для любых параметров для вызовов функций. сделанный main
, а затем убедитесь, что вы добавили достаточно байтов, чтобы значение делилось без остатка на 12. В нашем случае максимальное количество байтов, которое необходимо передать для любого вызова данной функции, составляет 4 байта. Затем мы прибавляем 8 к 4 (8+4=12), чтобы число делилось без остатка на 12. Затем мы вычитаем 12 из ESP в начале нашей функции.
Вместо использования PUSH для помещения параметров в стек теперь можно перемещать параметры непосредственно в стек в зарезервированное нами пространство. Поскольку мы не PUSH, стек не смещается. Поскольку мы не использовали PUSH, нам не нужно исправлять ESP после вызова нашей функции. Тогда код может выглядеть примерно так:
section .data
hello db "Hello, world", 0x0a, 0x00
section .text
default rel
global _main
extern _printf, _exit
_main:
sub esp, 12 ; 16-byte align stack + room for parameters passed
; to functions we call
mov [esp],dword hello ; First parameter at esp+0
call _printf
mov [esp], dword 0 ; First parameter at esp+0
call _exit
Если вы хотите передать несколько параметров, вы помещаете их в стек вручную, как мы сделали с одним параметром. Если бы мы хотели напечатать целое число 42 как часть нашего вызова printf
, мы могли бы сделать это следующим образом:
section .data
hello db "Hello, world %d", 0x0a, 0x00
section .text
default rel
global _main
extern _printf, _exit
_main:
sub esp, 12 ; 16-byte align stack + room for parameters passed
; to functions we call
mov [esp+4], dword 42 ; Second parameter at esp+4
mov [esp],dword hello ; First parameter at esp+0
call _printf
mov [esp], dword 0 ; First parameter at esp+0
call _exit
При запуске мы должны получить:
Привет, мир 42
16-байтовое выравнивание стека и кадр стека
Если вы хотите создать функцию с типичным фреймом стека, код в предыдущем разделе должен быть скорректирован. При входе в функцию в 32-битном приложении стек смещается на 4 байта, потому что адрес возврата был помещен в стек. Типичный пролог стекового фрейма выглядит так:
push ebp
mov ebp, esp
Помещение EBP в стек после входа в вашу функцию по-прежнему приводит к смещению стека, но теперь оно смещено на 8 байтов (4 + 4).
Из-за этого код должен вычесть 8 из ESP, а не 12. Кроме того, при определении пространства, необходимого для хранения параметров, локальных переменных стека и байтов заполнения для выравнивания, размер выделения стека должен быть равномерным. делится на 8, а не на 12. Код со фреймом стека может выглядеть так:
section .data
hello db "Hello, world %d", 0x0a, 0x00
section .text
default rel
global _main
extern _printf, _exit
_main:
push ebp
mov ebp, esp ; Set up stack frame
sub esp, 8 ; 16-byte align stack + room for parameters passed
; to functions we call
mov [esp+4], dword 42 ; Second parameter at esp+4
mov [esp],dword hello ; First parameter at esp+0
call _printf
xor eax, eax ; Return value = 0
mov esp, ebp
pop ebp ; Remove stack frame
ret ; We linked with C library that calls _main
; after initialization. We can do a RET to
; return back to the C runtime code that will
; exit the program and return the value in EAX
; We can do this instead of calling _exit
Поскольку вы связываетесь с библиотекой C в OS/X, она предоставит точку входа и выполнит инициализацию перед вызовом _main
. Вы можете вызвать _exit
, но вы также можете выполнить инструкцию RET с возвращаемым программой значением в EAX.
Еще одна потенциальная ошибка NASM?
Я обнаружил, что NASM v2.12 установлен через MacPorts на El Capitan, по-видимому, создает неправильные записи о перемещении для _printf
и _exit
, а при связывании с окончательным исполняемым файлом код не работает должным образом. Я наблюдал почти те же ошибки, что и вы, с исходным кодом.
Первая часть моего ответа по-прежнему относится к выравниванию стека, однако, похоже, вам также придется обойти проблему NASM. Один из способов сделать это — установить NASM, который поставляется с последними инструментами командной строки XCode. Эта версия намного старше и поддерживает только Macho-32 и не поддерживает директиву default
. Используя мой предыдущий код, выровненный по стеку, это должно работать:
section .data
hello db "Hello, world %d", 0x0a, 0x00
section .text
;default rel ; This directive isn't supported in older versions of NASM
global _main
extern _printf, _exit
_main:
sub esp, 12 ; 16-byte align stack
mov [esp+4], dword 42 ; Second parameter at esp+4
mov [esp],dword hello ; First parameter at esp+0
call _printf
mov [esp], dword 0 ; First parameter at esp+0
call _exit
Чтобы собрать с помощью NASM и связать с LD, вы можете использовать:
/usr/bin/nasm -f macho hello32x.asm -o hello32x.o
ld -macosx_version_min 10.8 -no_pie -arch i386 -o hello32x hello32x.o -lc
В качестве альтернативы вы можете связать с GCC:
/usr/bin/nasm -f macho hello32x.asm -o hello32x.o
gcc -m32 -Wl,-no_pie -o hello32x hello32x.o
/usr/bin/nasm
— это местонахождение версии инструментов командной строки XCode для NASM, которую распространяет Apple. Версия, которая у меня есть на El Capitan с последними инструментами командной строки XCode:
NASM версии 0.98.40 (Apple Computer, Inc., сборка 11), собранной 14 января 2016 г.
Я не рекомендую NASM версии 2.11.08, так как в ней есть серьезная ошибка, связанная с macho64. формат. Я рекомендую 2.11.09rc2. Я протестировал эту версию здесь, и, похоже, она работает правильно с приведенным выше кодом.
person
Michael Petch
schedule
13.03.2016
gcc
для ссылки. - person Jester   schedule 13.03.2016