【问题标题】:Print first character of string results in segmentation fault打印字符串的第一个字符导致分段错误
【发布时间】:2020-11-06 15:53:35
【问题描述】:

我正在尝试使用printf64bit Ubuntu 20 环境中将字符串str_1 的第一个字符打印到x86-assembly 中的标准输出,这是我的尝试:

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

some_proc:
    mov esi, str_1

    mov eax, [esi]
    push eax
    push argv_str
    call printf

    pop eax
    ret

main:
    call some_proc

    ret

section  .data
    str_1        db `three`
    argv_str     db `%c\n`

这个输出:

t
Segmentation fault (core dumped)

预期标准输出:

t

为什么这段代码会导致分段错误,如何修改代码以输出预期的标准输出?

【问题讨论】:

  • 您是否尝试过使用调试器查看故障实际发生的位置?
  • 通过让你的入口点是_start,你绕过了所有标准库的初始化代码,所以你不能指望任何标准库函数如printf能正常工作。这仅适用于根本不需要 C 库的程序,并且将通过原始系统调用完成所有工作。如果您需要 C 库,那么您需要将程序的入口点设为 main
  • 您还有一个错误,即mov eax, [esi] 在您只需要 1 个字节时会加载 4 个字节。
  • 哦,但实际上可能导致您崩溃的错误是您将参数推送到 printf 的堆栈中,并且您有责任将它们弹出,但您没有。
  • 我如何只加载一个字节?将代码的入口点更改为 main 并使用 gcc 编译后,我仍然收到分段错误。

标签: assembly x86 segmentation-fault


【解决方案1】:

你有几个错误:

  • 您将两个 4 字节的参数压入堆栈,用于 printf。在 SysV 调用约定中,printf 会将它们留在那里,因此您有责任在之后调整堆栈以“删除”它们。请记住,ret 会在栈顶寻找返回地址;正如您的代码所代表的那样,您推送的eax 中的字符值将是什么。那不是一个有效的地址,所以试图返回那里会导致段错误。您可以通过两次popping 删除这些参数,或者通过简单地将8 添加到esp 来更有效地删除这些参数,从而将堆栈指针移回原来的位置。

  • 当前版本的 i386 SysV ABI 要求堆栈在 call 执行任何函数之前对齐到 16 个字节。考虑到call 本身将 4 个字节作为返回地址压入堆栈,就像每条push 指令一样,您可以计算出调用some_procprintf 所需的必要调整,并且根据需要在esp 中添加或减去。 (从技术上讲,您可以避免在调用 some_proc 之前对齐堆栈,而只需在 printf 之前修复它,但这太容易搞砸了。)一些 32 位库可能会以这样的方式编译,而不是这个要求强制执行,但 64 位代码肯定需要它,所以遵守是一个好习惯。

  • esi 是根据i386 SysV ABI calling conventions 的被调用者保存寄存器(记住这些!)。如果你想修改它,你必须保存之前的内容并在返回之前恢复它们(例如函数顶部的push esi和末尾的pop esi)。或者选择一个调用者保存的寄存器,例如ecx。但是,如下所述,您根本不需要为str1 的地址使用寄存器。

  • mov eax, [esi] 是 32 位加载,因为 eax 是 32 位寄存器。所以这将使用来自位置str_1 的4 个字节加载eax,这将导致它包含值0x65726874(字节t h r e 作为小端整数)。这实际上可能不会导致问题,因为printf 应该将其int 参数转换回unsigned char 以进行打印,因此您应该只获得低字节0x74 = 't',但它仍然很奇怪,如果您的字符串非常短并且与未映射的页面相邻。

    更安全的是mov al, [esi],它只将一个字节加载到al,这是eax的低字节,但是高3字节中的任何垃圾都会留在那里。您可以使用xor eax, eax 预先将eax 归零,但您也可以使用movzx 指令用一块石头杀死两只鸟,该指令将较小的操作数零扩展为较大的操作数:movzx eax, byte [esi]

    当然,首先将地址放入esi 是多余的,因为可以将地址指定为立即数:mov al, [str_1]movzx eax, byte [str_1]。这样就无需保存/恢复esi

  • main 应该返回一个退出代码,并且返回值总是在eax 中。您的 eax 将包含您的字符,或者可能包含来自 printf 的返回值,具体取决于您的推送/弹出的位置。其中任何一个都是奇怪的非零退出代码,您的 shell 会认为程序遇到错误。所以在从main返回之前将eax清零,表示成功。

  • argv_str 是一个与argv 无关的字符串的奇怪名称。

我会修改你的程序如下:

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

some_proc:
    sub esp, 4 ; 8 more bytes pushed before call to printf
    movzx eax, byte [str_1]
    push eax
    push argv_str
    call printf
    add esp, 12
    ret

main:
    sub esp, 12
    call some_proc
    xor eax, eax
    add esp, 12
    ret

section  .data
    str_1        db `three`
    argv_str     db `%c\n`

【讨论】:

  • 如何知道在调用函数/过程之前你应该增加/减少多少堆栈?
  • @dnsis_445:基本上,您会根据对堆栈指针所做的其他更改来考虑如何使其成为 16 的倍数。请参阅 stackoverflow.com/a/64729675/634919 以及本网站上有关堆栈对齐的许多其他问题。
猜你喜欢
  • 1970-01-01
  • 2021-02-19
  • 2013-02-08
  • 1970-01-01
  • 1970-01-01
  • 2020-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多