为什么执行 "mov %%rsp, %%rbp" 会导致分段错误?

3

我是内嵌汇编的新手。我有以下带有内嵌汇编的C函数,我只是想查看push %%rbpmov %%rsp, %%rbp是否正常运行。我的函数如下:

test_inlineAssemblyFunction(){

    u64 base, rsp, base1, rsp1;
    asm volatile(

            "mov %%rbp, %0 \n"
            "mov %%rsp, %1 \n"

            "push %%rbp \n"
            "mov %%rbp, %2 \n"

            "mov %%rsp, %%rbp \n" <--- this line is causing the problem
            "mov %%rbp, %3 \n"
            :"=r"(base), "=r"(rsp),"=r"(base1), "=r"(rsp1)
            :
            : "rax","rbx","rsp"
            );
    printf("Before: Base register %x\n", base);
    printf("Before: stack pointer register %x\n", rsp);
    printf("After: (Should be same as previous )Base register %x\n", base1);
    printf("After: (Actually %rsp-8)Base register %x\n", rsp1);

    
}

输出:

Before: Base register ee050e50
Before: stack pointer register ee050e20
After: (Should be same as previous )Base register ee050e50
After: (Actually %rsp-8)Base register ee050e18
Segmentation fault (core dumped)

从输出信息看,所有的打印语句都输出了期望的结果。但是接下来出现了“段错误(Segmentation Fault)”。如果我的理解正确,“段错误”会在尝试读取或写入非法内存位置时发生。因此,在我的情况下,“mov %%rsp, %%rbp \n”导致了“段错误”,这意味着我不能从用户空间读取“rsp”。更新:正如@ Kuba没有忘记Monica所建议的那样,以下是“test_inlineAssemblyFunction”函数的反汇编代码。
(gdb) disas test_inlineAssemblyFunction
Dump of assembler code for function test_inlineAssemblyFunction:
0x0000000000007366 <+0>:    push   %rbp
0x0000000000007367 <+1>:    mov    %rsp,%rbp
0x000000000000736a <+4>:    push   %rbx
0x000000000000736b <+5>:    sub    $0x28,%rsp
0x000000000000736f <+9>:    mov    %rbp,%rdi
0x0000000000007372 <+12>:   mov    %rsp,%rsi
0x0000000000007375 <+15>:   push   %rbp
0x0000000000007376 <+16>:   mov    %rbp,%rcx
0x0000000000007379 <+19>:   mov    %rsp,%rbp
0x000000000000737c <+22>:   mov    %rax,%rdx
0x000000000000737f <+25>:   mov    %rdi,-0x30(%rbp)
0x0000000000007383 <+29>:   mov    %rsi,-0x28(%rbp)
0x0000000000007387 <+33>:   mov    %rcx,-0x20(%rbp)
0x000000000000738b <+37>:   mov    %rdx,-0x18(%rbp)
0x000000000000738f <+41>:   mov    -0x30(%rbp),%rax
0x0000000000007393 <+45>:   mov    %rax,%rsi
0x0000000000007396 <+48>:   lea    0x275ab(%rip),%rdi        # 0x2e948
0x000000000000739d <+55>:   mov    $0x0,%eax
0x00000000000073a2 <+60>:   callq  0x5570 <printf@plt>
0x00000000000073a7 <+65>:   mov    -0x28(%rbp),%rax
0x00000000000073ab <+69>:   mov    %rax,%rsi
0x00000000000073ae <+72>:   lea    0x275b3(%rip),%rdi        # 0x2e968
0x00000000000073b5 <+79>:   mov    $0x0,%eax
0x00000000000073ba <+84>:   callq  0x5570 <printf@plt>
0x00000000000073bf <+89>:   mov    -0x20(%rbp),%rax
0x00000000000073c3 <+93>:   mov    %rax,%rsi
0x00000000000073c6 <+96>:   lea    0x275c3(%rip),%rdi        # 0x2e990
0x00000000000073cd <+103>:  mov    $0x0,%eax
0x00000000000073d2 <+108>:  callq  0x5570 <printf@plt>
0x00000000000073d7 <+113>:  mov    -0x18(%rbp),%rax
0x00000000000073db <+117>:  mov    %rax,%rsi
0x00000000000073de <+120>:  lea    0x275e3(%rip),%rdi        # 0x2e9c8
0x00000000000073e5 <+127>:  mov    $0x0,%eax
0x00000000000073ea <+132>:  callq  0x5570 <printf@plt>
0x00000000000073ef <+137>:  nop
0x00000000000073f0 <+138>:  mov    -0x8(%rbp),%rbx
0x00000000000073f4 <+142>:  leaveq 
0x00000000000073f5 <+143>:  retq   
End of assembler dump.
(gdb)

你有没有尝试查看编译器输出的汇编代码,以了解你的汇编代码与编译器的混合情况?根据是否使用“-fomit-frame-pointer”,“=m”操作数将相对于RSP或RBP引用,因此在具有内存操作数的asm语句中搞乱RSP和RBP似乎是一个可怕的想法。(而且,我认为RSP破坏实际上是不起作用的)。此外,在inline-asm中踩到RSP下面的红区是不安全的,但在实践中,像这样的非叶函数是安全的:gcc和clang不会使用res-zone,因为你调用printf。 - Peter Cordes
2
阅读 rsp 是没有问题的,但在内联汇编中修改 rbp 而不使用 clobber 是不安全的。在 x86-64 内联汇编中将数据推入栈中也不安全,因为存在红色区域。而且,在栈上保留东西并使栈指针处于不同位置是绝对不安全的(clobber rsp 也无济于事)。 - Nate Eldredge
@PeterCordes 抱歉,我修复了我的代码。我尝试使用=r=m两种方式。=m导致堆栈溢出,而=r导致分段错误。 - user45698746
1
你修改了RSP但没有恢复,所以当函数尝试返回时,可能是因为RSP没有指向返回地址。 "rsp" clobber并不能保证安全;我相信它是被支持的。再次查看编译器的整个汇编输出,例如如何从GCC / clang汇编输出中删除“噪音”? - Peter Cordes
@NateEldredge,您能否给一个例子来说明如何使用clobber修改rbp - user45698746
2个回答

2

没有任何保证你编译代码所使用的编译器会按照你暗示的方式使用寄存器。这就是全部。我不确定你从哪里得到了这个想法,认为它应该按照你的意愿进行,而不是实际查看编译器生成的汇编输出。

当你在汇编中编写代码时,你与所有周围的汇编代码进行交互,因此我很困惑为什么你要在黑暗中进行操作。你必须首先停下来,弄清楚如何从编译器获得汇编输出,即如何传递编译器期望的选项以将这种汇编输出保存到文件中,并将目标代码保存到文件中。然后重新构建程序并查看你的汇编代码如何与其余部分配合。提示:它不会配合 :)

如果在你自己的系统上弄清楚这一点有点太分散了,那么你可以使用https://godbolt.org编写代码,选择其中一个gcc编译器,然后你既可以检查汇编输出,也可以在云端运行程序并观察效果。你只需要一个网页浏览器。不需要安装其他任何东西。


我已经添加了编译器生成的汇编代码。正如@Nate Eldredge建议的那样,我面临的问题是由于修改了rbp 导致的。我尝试用rax替换rbp,然后函数运行没有错误。 - user45698746
@user45698746 你不能随意修改rbp寄存器。这不是像你看到可用的寄存器,然后扔飞镖选中rbp,而且可以使用其他东西代替。你试图模仿以前看过的代码。但你只得到了那段代码的前半部分:如果你添加缺失的第二部分,你所展示的代码将完美地工作。你写的通常用作函数序言来建立堆栈帧。每个序言都需要一个尾声:在从函数返回之前进行相反的操作。你构建的堆栈帧必须被拆除。然后它就会起作用 :) - Kuba hasn't forgotten Monica
@Kubahasn'tforgottenMonica:在非naked函数内部创建另一个“堆栈帧”只有在您的输入或输出操作数中没有任何一个是“m”或“=m”时才能正常工作。否则,GCC将使用%0填充您的模板,例如-4(%rbp)12(%rsp),具体取决于是否使用了-fomit-frame-pointer。但是,如果您避免任何“m”或“g”操作数,并且该函数不使用红区(例如因为它不是叶子函数),那么您可以勉强安全地在asm模板中使用堆栈,就像您所说的那样,只要在asm语句结束之前将所有内容放回去即可。 - Peter Cordes
(我提到了内存操作数,因为问题最初涉及它们。) - Peter Cordes

2
问题不在于您读取了rsp,而是您在asm块中覆盖了rbp(通过将从rsp复制的值覆盖它)和rsp(通过执行没有匹配的poppush),而没有通知编译器有关这些clobbered的信息。前者可以通过将rbp添加到clobber列表中来解决;后者根本不合法。

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