更新:果然,这是最新版本的nasm中的一个错误。我“降级”并在修复我的代码后,如我接受的答案所示,一切正常。谢谢大家!
我在OS X上的32位汇编程序中应该是一个非常简单的程序有问题。
首先,代码:
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
push hello
call _printf
push 0
call _exit
它汇编和链接,但当我运行可执行文件时,它崩溃并出现分段错误:11。
汇编和链接的命令行是:
nasm -f macho32 hello32x.asm -o hello32x.o
我知道-o没有必要100%
链接:
ld -lc -arch i386 hello32x.o -o hello32x
当我把它运行到lldb来调试它时,一切都很好,直到它进入对_printf的调用,它会崩溃,如下所示:
(lldb) s
Process 1029 stopped
* thread #1: tid = 0x97a4, 0x00001fac hello32x`main + 8, queue = 'com.apple.main-thread', stop reason = instruction step into
frame #0: 0x00001fac hello32x`main + 8
hello32x`main:
-> 0x1fac <+8>: calll 0xffffffff991e381e
0x1fb1 <+13>: pushl $0x0
0x1fb3 <+15>: calll 0xffffffff991fec84
0x1fb8: addl %eax, (%eax)
(lldb) s
Process 1029 stopped
* thread #1: tid = 0x97a4, 0x991e381e libsystem_c.dylib`vfprintf + 49, queue = 'com.apple.main-thread', stop reason = instruction step into
frame #0: 0x991e381e libsystem_c.dylib`vfprintf + 49
libsystem_c.dylib`vfprintf:
-> 0x991e381e <+49>: xchgb %ah, -0x76f58008
0x991e3824 <+55>: popl %esp
0x991e3825 <+56>: andb $0x14, %al
0x991e3827 <+58>: movl 0xc(%ebp), %ecx
(lldb) s
Process 1029 stopped
* thread #1: tid = 0x97a4, 0x991e381e libsystem_c.dylib`vfprintf + 49, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x890a7ff8)
frame #0: 0x991e381e libsystem_c.dylib`vfprintf + 49
libsystem_c.dylib`vfprintf:
-> 0x991e381e <+49>: xchgb %ah, -0x76f58008
0x991e3824 <+55>: popl %esp
0x991e3825 <+56>: andb $0x14, %al
0x991e3827 <+58>: movl 0xc(%ebp), %ecx
正如您可以看到的那样,它会因访问错误而停止。
答案 0 :(得分:4)
代码的一个严重问题是堆栈对齐。在进行 CALL 时,32位OS / X代码需要16字节堆栈对齐。 Apple IA-32 Calling Convention说明了这一点:
IA-32环境中使用的函数调用约定与System V IA-32 ABI中使用的函数调用约定相同,但以下情况除外:
- 返回结构的不同规则
- 堆栈在函数调用
处是16字节对齐- 大数据类型(大于4个字节)保持自然对齐
- 大多数浮点运算使用SSE单元而不是x87 FPU执行,除非在长双精度值上运行。 (IA-32环境默认为x87 FPU的64位内部精度。)
从 ESP 中减去12,使堆栈与16字节边界对齐(4个字节表示返回地址+ 12 = 16)。问题是,当你对一个函数进行 CALL 时,堆栈必须在 CALL 之前对齐16个字节。不幸的是,在调用printf
和exit
之前,您会推送4个字节。当堆栈应该对齐到16个字节时,这会使堆栈错位4。您必须以正确的对齐方式重新编写代码。同样,您必须在打电话后清理堆栈。如果您使用 PUSH 将参数放入堆栈,则需要在 CALL 之后调整 ESP 以将堆栈恢复到之前的状态。
修复代码的一种天真的方式(不是我的建议)就是这样做:
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 放置在堆栈上的参数。 main
返回地址为4个字节,我们进行堆栈调整为8个字节, DWORD 参数为4个字节对齐为4个字节。
更好的方法是计算main
函数中所有基于堆栈的局部变量(在本例中为0)所需的堆栈空间量,以及您将使用的最大字节数需要任何参数来调用main
进行的调用,然后确保填充足够的字节以使值可以被12整除。在我们的例子中,任何给定函数调用需要推送的最大字节数是4字节。然后我们添加8到4(8 + 4 = 12)以便可以被12整除。然后我们在函数开始时从 ESP 中减去12。
现在,您可以将参数直接移动到堆栈中,而不是使用 PUSH 将参数放入堆栈中,而不是使用 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
如果您想传递多个参数,请将它们手动放在堆栈上,就像我们使用单个参数一样。如果我们想要在printf
调用中打印整数42,我们可以这样做:
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
如果您要创建具有典型堆栈帧的函数,则必须调整上一节中的代码。在进入32位应用程序中的函数时,堆栈未对齐4个字节,因为返回地址被放置在堆栈上。典型的堆栈框架序言如下:
push ebp
mov ebp, esp
在输入函数后将 EBP 推入堆栈仍会导致堆栈错位,但现在错位8字节(4 + 4)。
因为代码必须从 ESP 而不是12中减去8.当确定保存参数所需的空间,本地堆栈变量和用于对齐的填充字节时,堆栈分配大小将具有可以被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
因为您与OS / X上的 C 库链接,它将提供一个入口点并在调用_main
之前进行初始化。您可以调用_exit
,但也可以使用 EAX 中程序的返回值执行 RET 指令。
我发现 El Capitan 上通过MacPorts安装的 NASM v2.12似乎会为_printf
和{{1}生成错误的重定位条目当链接到最终的可执行文件时,代码没有按预期工作。我观察到你用原始代码几乎完全相同的错误。
我的答案的第一部分仍然适用于堆栈对齐,但是看起来您还需要解决 NASM 问题。一种方法是安装最新的XCode命令行工具附带的 NASM 。这个版本更老了,只支持Macho-32,并且不支持_exit
指令。使用我以前的堆栈对齐代码,这应该有效:
default
要与 NASM 组合并与 LD 链接,您可以使用:
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
或者您可以链接 GCC :
/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
/usr/bin/nasm -f macho hello32x.asm -o hello32x.o
gcc -m32 -Wl,-no_pie -o hello32x hello32x.o
是Apple分发的 NASM 的XCode命令行工具版本的位置。我使用最新的XCode命令行工具在 El Capitan 上的版本是:
2016年1月14日编译的NASM版本0.98.40(Apple Computer,Inc。build 11)
我不推荐 NASM 版本2.11.08,因为它有一个与macho64格式相关的serious bug。我推荐2.11.09rc2。我在这里测试了这个版本,它似乎与上面的代码一起正常工作。