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

Assembly 打印字符串的第一个字符会导致分段错误,assembly,x86,segmentation-fault,Assembly,X86,Segmentation Fault,我正在尝试使用printf在64位Ubuntu 20环境中,将字符串stru 1的第一个字符打印到x86汇编中的标准输出,以下是我的尝试: ; 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

我正在尝试使用
printf
64位
Ubuntu 20
环境中,将字符串
stru 1
的第一个字符打印到
x86汇编
中的标准输出,以下是我的尝试:

; 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
    将在堆栈顶部查找返回地址;当您的代码保持不变时,将出现您推送的
    eax
    中的字符值。这不是一个有效的地址,因此尝试返回该地址会导致segfault。您可以通过两次ping
    pop
    来删除这些参数,或者通过简单地将
    8
    添加到
    esp
    ,从而将堆栈指针移回原来的位置来更有效地删除这些参数

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

  • esi
    是被调用方根据(记住这些!)保存的寄存器。如果要修改它,必须保存以前的内容并在返回之前还原它们(例如,在函数顶部按esi,在最后按esi)。或者选择调用者保存的寄存器,例如
    ecx
    。但是,如下文所述,您根本不需要为
    str1
    的地址使用寄存器

  • mov-eax,[esi]
    是32位加载,因为
    eax
    是32位寄存器。因此,这将从位置
    stru 1
    加载
    eax
    的4个字节,这将导致它包含值
    0x65726874
    (字节
    th re
    作为一个小的endian整数)。这实际上可能不会导致问题,因为
    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`


您是否尝试过使用调试器查看故障实际发生的位置?通过将入口点设置为
\u start
,您绕过了所有标准库的初始化代码,因此您不能期望任何标准库函数(如
printf
)正常工作。这只适用于完全不需要C库的程序,并且将通过原始系统调用完成所有工作。如果你需要C库,那么你需要让你的程序的入口点是
main
。在
mov eax中,你有另一个bug,[esi]
在你只需要1的时候加载4个字节。哦,但实际上可能导致你崩溃的bug是你把printf的参数推到堆栈上,你有责任把它们放回去,但是你没有。我怎么只加载一个字节?在将代码的入口点更改为
main
并使用
gcc
编译之后,我仍然收到一个分段错误。在调用函数/过程之前,如何确切地知道堆栈的增量/减量是多少?@dnsis_445:基本上,您考虑如何使其成为16的倍数,基于对堆栈指针所做的其他更改。请参阅本网站上有关堆栈对齐的其他问题。