16 字节堆栈对齐
您的代码的一个严重问题是堆栈对齐。 32 位 OS/X 代码在创建时需要 16 字节堆栈对齐CALL. The Apple IA-32 调用约定说:
IA-32 环境中使用的函数调用约定与系统 V IA-32 ABI,但以下情况除外:
- 返回结构的不同规则
- The 堆栈是 16 字节对齐的在函数调用点
- 大数据类型(大于 4 字节)保持自然对齐
- 大多数浮点运算都是使用 SSE 单元而不是 x87 FPU 执行的,除非对长双精度值进行运算。 (IA-32 环境默认为 x87 FPU 的 64 位内部精度。)
你减去 12ESP将堆栈与 16 字节边界对齐(4 字节用于返回地址 + 12 = 16)。问题是当你制作一个CALL对于函数来说,堆栈必须在函数之前对齐 16 个字节CALL本身。不幸的是,您在调用之前推送了 4 个字节printf
and exit
。当堆栈应该对齐到 16 字节时,这会使堆栈错位 4 个字节。您必须重新编写代码并进行适当的对齐。调用后还必须清理堆栈。如果你使用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
and printf
) 正好需要一个DWORD被放置在参数堆栈上。 4 个字节用于main
的返回地址,8为我们所做的堆栈调整,4为DWORD参数 = 16 字节对齐。
更好的方法是计算您的所有基于堆栈的局部变量(在本例中为 0)所需的堆栈空间量。main
函数,加上函数调用的任何参数所需的最大字节数main
然后确保填充足够的字节以使该值可以被 12 整除。在我们的例子中,任何一个给定函数调用需要推送的最大字节数是 4 个字节。然后我们将 8 加 4 (8+4=12) 即可被 12 整除。然后我们减去 12ESP在我们的函数开始时。
而不是使用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
Pushing EBP进入函数后进入堆栈仍然会导致堆栈未对齐,但现在已错位 8 个字节 (4 + 4)。
因此,代码必须从中减去 8ESP而不是 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
因为您链接到COS/X 上的库它将提供一个入口点并在调用之前进行初始化_main
。您可以致电_exit
但你也可以做一个RET程序返回值的指令EAX.
另一个潜在的 NASM 错误?
我发现NASMv2.12 安装通过MacPorts on 埃尔卡皮坦似乎生成了不正确的重定位条目_printf
and _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 命令行工具版本的位置NASMApple 分发的。我用的版本埃尔卡皮坦最新的 XCode 命令行工具是:
NASM 版本 0.98.40(Apple Computer, Inc. build 11)于 2016 年 1 月 14 日编译
我不推荐NASM版本 2.11.08 因为它有严重的错误与 macho64 格式相关。我建议2.11.09rc2。我已经在这里测试了该版本,它似乎确实可以与上面的代码一起正常工作。