什么导致程序出现分段错误?

3

我正在尝试计算将数据推送到栈中的大小。在尝试时,我收到了一个分段错误。我的目标是在 push 任何数据到栈上之前将 ebp 指向 esp。在我将数据推入栈后,我用 ebp - esp 计算已推入数据的大小(以字节为单位),并将其存储在 ebx 中,并使用 printf 将其输出到标准输出。

例如:

; compile with:
; nasm -f elf test.asm && gcc -m32 -o test test.o
section .text
global  main
extern printf

main:
    ; set the frame pointer to the beginning of the stack before-
    ; data is pushed.
    push  ebp
    mov   ebp, esp

    push  ebx
    push  0x00       ; <- null terminating byte/string truncation
    push  0x64636261 ; <- data

    mov   ebx, ebp
    sub   ebx, esp   ; start of stack - end of stack  = sizeof(data) stored in ebx

    push  ebx
    push  format_str
    call  printf
    add   esp, 8

    pop   ebp
    pop   ebx
    ret

section .data
format_str db "%s", 2, 0

当我编译此代码时,输出如下:

Segmentation fault (core dumped)

预期输出:

5

1
我猜你想要 %d... - CherryDT
1
您的字符串数据从堆栈中未被删除。您缺少mov esp, ebp语句。 - Jester
1
抱歉,这是我的错误。不过,这仍然无法解决分段错误的问题。有什么想法吗? - asd_2323
Jester,你的答案是正确的。它输出了12而不是5,你有什么想法为什么会这样? - asd_2323
12 是正确的。你推了3个东西,每个4字节。ebx 也是4字节,而 push 0x00 将推送4个零而不是一个。 - Jester
显示剩余6条评论
1个回答

3
首先,如果您想使项目回到其原始寄存器中,则应按与它们被推入相反的顺序弹出它们。以下是删除不相关行后的代码段:
push ebp
push ebx
pop ebp
pop ebx

不恢复ebp的先前值,则由于向上移动堆栈帧可能会导致问题,这是非常可能发生的。


此外,最好遵循从ebp中恢复esp的正常惯例,而不是盲目地加上八。最终代码应该类似于:

; add esp, 8 ; not the normal way.
mov   esp, ebp
pop   ebp

采用这种方法可以避免手动计算需要从栈中弹出多少字节的需求,因为你的计算是错误的,因为你没有考虑到你所推入的所有内容。


在此之前,你必须确保 esp 在正确的位置,以便 pop ebp 能够工作。这意味着你要弹出所有你推入的东西(除了 ebp 本身)。由于你已经推入了(在 ebp 后面)ebx0x000000000c64636261ebxformat_str,你应该确保在尝试弹出 ebp 之前,所有这些都不在栈中。

考虑到所有这些,你可以得到如下代码:

main:
    push  ebp
    mov   ebp, esp

    push  ebx           ; (+1)
    push  0x00          ; (+2)
    push  0x64636261    ; (+3)

    mov   ebx, ebp
    sub   ebx, esp

    push  ebx           ; (+4)
    push  format_str    ; (+5)
    call  printf
    add   esp, 16       ; (-5, -4, -3, -2)
    pop   ebx           ; (-1)

    mov   esp, ebp
    pop   ebp
    ret

每个(+N)注释代表已经被推到堆栈上的32位值,而(-N)注释表示哪些指令将逆转这些推送操作。add esp, 16逆转其中四个(每个四个字节),这样做是因为我们不关心这些项发生了什么。剩下的就是最终的pop操作来恢复原始的ebx(这是我们关心的)。
我认为在这种情况下,最后重新加载esp是不必要的,因为它已经通过之前的步骤恢复到正确的值。是否保留它以保持入口/出口一致性由您决定。

1
看起来不错。另一种选择是不使用 ebx,而是使用一些类似于 edx 的调用者保存寄存器,这样整个混乱就可以减少。 - Jester
那么实质上,每个push指令都会将esp减4? - asd_2323
1
asd_2323:对于你的每个push指令,是的。然而,如果你做类似于push ax(在x86或x64中),只有两个字节会被推送。这取决于你推送的内容,这也受到体系结构的限制(例如,我不认为你可以在x64上推送四个字节,尽管我没有仔细研究过)。 - paxdiablo
1
唯一的小问题是对 printf 的调用不符合更现代的 Linux i386 System V ABI,这需要在 printf 函数调用点处进行 16 字节对齐。我认为它的偏移量为 4 字节。 - Michael Petch
在这里,mov esp, ebp 是无意义的;为了使用 pop ebx 恢复正确的内容,ESP 必须已经指向保存的 EBX,你知道它就在保存的 EBP 的下面。GCC 可能会使用 mov ebx, [ebp-4] / leave(leave 包括 mov esp, ebp)或 add esp,16 / pop ebx/pop ebp - Peter Cordes
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接