打印字符串的第一个字符导致分段错误

Print first character of string results in segmentation fault

我正在尝试使用 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

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

您有几个错误:

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

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

  • esi 是根据 i386 SysV ABI calling conventions 的 callee-saved 寄存器(记住这些!)。如果你想修改它,你必须保存以前的内容并在 returning 之前恢复它们(例如函数顶部的 push esi 和最后的 pop esi )。或者选择 caller-saved 寄存器,例如 ecx。但是,如下所述,您实际上根本不需要为 str1 地址使用寄存器。

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

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

    当然,先将地址放入esi是多余的,因为地址可以指定为立即数:mov al, [str_1]movzx eax, byte [str_1]。这样就避免了 save/restore esi.

  • main 需要 return 退出代码,return 值总是进入 eax。您的 eax 将包含您的角色或 printf 中的 return 值,具体取决于您的 push/pops 的最终位置。其中任何一个都是奇怪的非零退出代码,您的 shell 会认为程序遇到错误。因此,在从 main 进行 return 之前将 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`