分段错误 11 链接 os x 32 位汇编器

Segmentation Fault 11 linking os x 32-bit assembler

更新:果然,这是 nasm 最新版本中的一个错误。我 "downgraded" 并按照我接受的答案中所示修复我的代码后,一切正常。谢谢大家!

我在 OS X 上的 32 位 assembler 中应该是一个非常简单的程序时遇到问题。

一、代码:

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

它 assembles 和 links,但是当我 运行 可执行文件崩溃时出现分段错误:11.

assemble 和 link 的命令行是:

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  [=14=]x0
      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   [=14=]x14, %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   [=14=]x14, %al
      0x991e3827 <+58>: movl   0xc(%ebp), %ecx

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

16 字节堆栈对齐

您的代码的一个严重问题是堆栈对齐。 32 位 OS/X 代码在您进行 CALL 时需要 16 字节堆栈对齐。 Apple IA-32 Calling Convention 表示:

The function calling conventions used in the IA-32 environment are the same as those used in the System V IA-32 ABI, with the following exceptions:

  • Different rules for returning structures
  • The stack is 16-byte aligned at the point of function calls
  • Large data types (larger than 4 bytes) are kept at their natural alignment
  • Most floating-point operations are carried out using the SSE unit instead of the x87 FPU, except when operating on long double values. (The IA-32 environment defaults to 64-bit internal precision for the x87 FPU.)

您从 ESP 中减去 12 以将堆栈对齐到 16 字节边界(4 字节用于 return 地址 + 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 的 return 地址,8 个用于我们进行的堆栈调整,4 个用于 DWORD 参数 = 16 字节对齐。


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

您现在可以将参数直接移动到我们保留的 space 中,而不是使用 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

当 运行 我们应该得到:

Hello, world 42


16 字节堆栈对齐和堆栈帧

如果您要创建具有典型堆栈帧的函数,则必须调整上一节中的代码。在进入 32 位应用程序中的函数时,堆栈未对齐 4 个字节,因为 return 地址被放置在堆栈上。典型的栈帧序言如下所示:

push ebp
mov  ebp, esp

在进入您的函数后将 EBP 压入堆栈仍然会导致堆栈未对齐,但现在已错位 8 个字节 (4 + 4)。

因此,代码必须从 ESP 中减去 8 而不是 12。在确定 space 需要保存参数、局部堆栈变量和填充时用于对齐的字节 堆栈分配大小必须能被 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

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


又一个潜在的 NASM 错误?

我发现 NASM v2.12 通过 MacPorts 安装在 El Capitan 上似乎生成不正确_printf_exit 的重定位条目,当 linked 到最终的可执行文件时,代码无法按预期工作。我观察到与您使用原始代码所做的错误几乎相同。

我的回答的第一部分仍然适用于堆栈对齐,但看来您还需要解决 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

assemble 与 NASM 和 link 与 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 

或者您可以 link 使用 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 version 0.98.40 (Apple Computer, Inc. build 11) compiled on Jan 14 2016

我不推荐 NASM 版本 2.11.08 因为它有一个 related to macho64 format. I recommend 2.11.09rc2。我已经在此处测试了该版本,它似乎与上面的代码一起正常工作。