【问题标题】:Segmentation Fault 11 linking os x 32-bit assemblerSegmentation Fault 11 链接 os x 32 位汇编器
【发布时间】:2016-06-28 02:30:53
【问题描述】:

更新:果然,这是最新版本的 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

正如您在底部看到的那样,由于访问错误而停止。

【问题讨论】:

  • 我很确定您在链接期间至少也会收到警告。无论如何,这看起来像是不让 C 库初始化的常见情况。使用gcc 链接。
  • 嗨@Jester。是的,我收到一个错误:ld:警告:PIE 已禁用。在代码签名的 PIE 中不允许使用绝对寻址(可能是 -mdynamic-no-pic),但在 hello32x.o 的 _main 中使用。要解决此警告,请不要使用 -mdynamic-no-pic 进行编译或使用 -Wl,-no_pie 进行链接,但如果我使用 gcc 进行链接,则会出现相同的错误。
  • 我在我的代码中推断出一个错误 - 我应该只完成 sub esp, 8,因为将字符串的地址推入堆栈会将其带到 12,然后将返回地址推入该调用将其变为 16,使 OS X 需要 16 字节的堆栈对齐。

标签: macos assembly x86 32-bit libc


【解决方案1】:

16 字节堆栈对齐

代码的一个严重问题是堆栈对齐。 32 位 OS/X 代码在您发出 CALL 时需要 16 字节堆栈对齐。 Apple IA-32 Calling Convention 是这样说的:

IA-32 环境中使用的函数调用约定与System V IA-32 ABI 中使用的相同,但有以下例外:

  • 返回结构的不同规则
  • 堆栈在函数调用时是 16 字节对齐的
  • 大型数据类型(大于 4 个字节)保持自然对齐方式
  • 大多数浮点运算是使用 SSE 单元而不是 x87 FPU 执行的,除非在 long double 值上运行。 (对于 x87 FPU,IA-32 环境默认为 64 位内部精度。)

您从 ESP 中减去 12 以将堆栈对齐到 16 字节边界(返回地址为 4 字节 + 12 = 16)。问题是,当您对函数进行 CALL 时,堆栈必须在 CALL 本身之前对齐 16 个字节。不幸的是,您在调用 printfexit 之前推送了 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

上面的代码之所以有效,是因为我们可以利用这两个函数(exitprintf)都需要将一个 DWORD 放在堆栈中作为参数的事实。 4 个字节用于main 的返回地址,8 个用于我们所做的堆栈调整,4 个用于 DWORD 参数 = 16 字节对齐。


一个更好的方法是在main 函数中计算所有基于堆栈的局部变量(在本例中为 0)所需的堆栈空间量,以及您需要的最大字节数main 进行的函数调用的任何参数,然后确保填充足够的字节以使该值可以被 12 整除。在我们的例子中,任何一个给定的函数调用需要推送的最大字节数是 4 个字节。然后我们将 8 加到 4 (8+4=12) 以被 12 整除。然后我们在函数开始时从 ESP 中减去 12。

您现在可以将参数直接移到堆栈上,而不是使用 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)。

因为代码必须从 ESP 中减去 8 而不是 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

因为您与 OS/X 上的 C 库链接,它将提供一个入口点并在调用 _main 之前进行初始化。您可以调用_exit,但您也可以使用EAX 中的程序返回值执行RET 指令。


还有另一个潜在的 NASM 错误?

我发现在 El Capitan 上通过 MacPorts 安装的 NASM v2.12 似乎会为 _printf_exit 生成错误的重定位条目,并且在链接时对于最终的可执行文件,代码无法按预期工作。我观察到几乎与原始代码相同的错误。

我的回答的第一部分仍然适用于堆栈对齐,但是您似乎也需要解决 NASM 问题。一种方法是安装最新 XCode 命令行工具附带的 NASM。这个版本要旧得多,只支持 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 是 Apple 分发的 NASM 的 XCode 命令行工具版本的位置。我在 El Capitan 上使用最新 XCode 命令行工具的版本是:

NASM 版本 0.98.40(Apple Computer, Inc. build 11)于 2016 年 1 月 14 日编译

我不推荐 NASM 版本 2.11.08,因为它有一个与 macho64 格式相关的 serious bug。我推荐2.11.09rc2。我在这里测试了那个版本,它似乎可以与上面的代码一起正常工作。

【讨论】:

  • 是的,这可能也是一个问题,但从 gdb 日志来看,这似乎不是对齐问题。通常这只会在他们使用浮点数时才会咬人,因为printf 可能会使用对齐的 SSE 指令。
  • @jester OS/X 函数(尤其是在 LIBC 中)对堆栈对齐非常挑剔,即使根本不使用 SSE 也会遇到问题。跨度>
  • 执行到奇怪的地方肯定不是问题吗?无论如何,如果你发誓你用ld -lc检查过这个,我会相信你的话,就像 OP 所做的那样,它确实有效。
  • @Jester :我见过陌生人!实际上,我自己也见过很多次。当我回答这个问题时,我能够重现所描述的 OPs 问题,并且当堆栈正确对齐时,事情按预期工作。我看到了您对 LIBC 初始化的评论。 OP 做事的方式实际上是与 LIBC 正确链接,而 C 运行时将进行初始化,然后调用 _main 。正如您所指出的,他可以通过使用 GCC 来简化链接。
  • 我已经“降级”,经过测试,而且效果很好。谢谢,@MichaelPetch!
猜你喜欢
  • 2016-06-28
  • 2014-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-28
  • 1970-01-01
相关资源
最近更新 更多