首页 > 其他分享 >从__builtin_eh_return看callee saved register

从__builtin_eh_return看callee saved register

时间:2024-03-04 20:35:57浏览次数:23  
标签:__ return eh mov rbp push rdi rsi

问题

C++的异常处理看起来是一个比较神奇的功能,能够在运行时穿越堆栈,从异常发生位置直达异常处理位置。通过gcc的代码可以看到,这个堆栈回溯的一个关键步骤是这个宏,其中又使用了gcc的内置指令__builtin_eh_return。网上关于__builtin_eh_return这个内置函数的资料较少,结合gcc的源代码可以猜测,这个内置函数的主要功能是和常规的return指令的功能类似,主要就是在函数结束的时候恢复该函数修改的(clobbered)寄存器,也就是所谓的callee saved registers。


/* Install TARGET into CURRENT so that we can return to it.  This is a
   macro because __builtin_eh_return must be invoked in the context of
   our caller.  */

#define uw_install_context(CURRENT, TARGET)				\
  do									\
    {									\
      long offset = uw_install_context_1 ((CURRENT), (TARGET));		\
      void *handler = uw_frob_return_addr ((CURRENT), (TARGET));	\
      _Unwind_DebugHook ((TARGET)->cfa, handler);			\
      __builtin_eh_return (offset, handler);				\
    }									\
  while (0)

测试

SO上说明callee是需要保留EBP//ESI/EDI寄存器的,连超级大佬Raymond Chen都来站台

The Windows and SystemV calling convention for x86-32 requires functions to preserve the ebx, esi, edi, and ebp registers. But these are just conventions. – Raymond Chen

但是简单测试下并非如此,生成的代码中明显修改了rsi寄存器的值,但是在__builtin_eh_return指令中并没有恢复rsi寄存器的内容。

        movq    %rdx, %rsi
        movq    %rax, %rdi

完整测试栗子如下

tsecer@harry: cat eh_return.c 
void foo(long xx, void * yy)
{
    extern int bar(long, long);
    bar(xx, xx);
    __builtin_eh_return (1111, (void*)(2222L));
}
tsecer@harry: g++ -S eh_return.c 
tsecer@harry: cat eh_return.s 
        .file   "eh_return.c"
        .text
        .globl  _Z3foolPv
        .type   _Z3foolPv, @function
_Z3foolPv:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        pushq   %rdx
        pushq   %rax
        subq    $16, %rsp
        .cfi_offset 1, -24
        .cfi_offset 0, -32
        movq    %rdi, -24(%rbp)
        movq    %rsi, -32(%rbp)
        movq    -24(%rbp), %rdx
        movq    -24(%rbp), %rax
        movq    %rdx, %rsi
        movq    %rax, %rdi
        call    _Z3barll
        movl    $1111, %edx
        movl    $2222, %eax
        movq    %rdx, %rcx
        movq    %rax, 8(%rbp,%rcx)
        movq    -16(%rbp), %rax
        movq    -8(%rbp), %rdx
        leaq    8(%rbp,%rcx), %rcx
        movq    0(%rbp), %rbp
        .cfi_restore 6
        .cfi_def_cfa 2, 8
        movq    %rcx, %rsp
        ret
        .cfi_endproc
.LFE0:
        .size   _Z3foolPv, .-_Z3foolPv
        .section        .note.GNU-stack,"",@progbits
tsecer@harry: 

如果觉得这个测试有问题,可以看下gcc自带库中的代码,从汇编代码可以看到,在函数的序言(prelogue)中是修改了rsi和rdi的值,但是在执行__builtin_eh_return函数的时候只是恢复了rbx、和r12——r15,明显没有恢复被修改了的rsi和rdi寄存器。

(gdb) disas
Dump of assembler code for function _Unwind_RaiseException:
   0x00007ffff73431a0 <+0>:     push   %rbp
   0x00007ffff73431a1 <+1>:     mov    %rsp,%rbp
   0x00007ffff73431a4 <+4>:     push   %r15
   0x00007ffff73431a6 <+6>:     push   %r14
   0x00007ffff73431a8 <+8>:     push   %r13
   0x00007ffff73431aa <+10>:    push   %r12
   0x00007ffff73431ac <+12>:    lea    -0x3a0(%rbp),%r14
   0x00007ffff73431b3 <+19>:    push   %rbx
   0x00007ffff73431b4 <+20>:    push   %rdx
   0x00007ffff73431b5 <+21>:    lea    0x10(%rbp),%rsi
   0x00007ffff73431b9 <+25>:    push   %rax
   0x00007ffff73431ba <+26>:    mov    %rdi,%r12
   0x00007ffff73431bd <+29>:    mov    %r14,%rdi
   0x00007ffff73431c0 <+32>:    lea    -0x1c0(%rbp),%r13
###.....
   0x00007ffff734344b <+683>:   call   0x7ffff7342f00 <_Unwind_RaiseException_Phase2>
   0x00007ffff7343450 <+688>:   cmp    $0x7,%eax
   0x00007ffff7343453 <+691>:   jne    0x7ffff7343310 <_Unwind_RaiseException+368>
   0x00007ffff7343459 <+697>:   mov    %rbx,%rsi
   0x00007ffff734345c <+700>:   mov    %r14,%rdi
   0x00007ffff734345f <+703>:   call   0x7ffff7340d80 <uw_install_context_1>
   0x00007ffff7343464 <+708>:   mov    -0x218(%rbp),%r8
   0x00007ffff734346b <+715>:   mov    -0x220(%rbp),%rdi
   0x00007ffff7343472 <+722>:   mov    %r8,%rsi
   0x00007ffff7343475 <+725>:   call   0x7ffff7343190 <_Unwind_DebugHook>
=> 0x00007ffff734347a <+730>:   mov    %rax,%rcx
   0x00007ffff734347d <+733>:   mov    %r8,0x8(%rbp,%rax,1)
   0x00007ffff7343482 <+738>:   mov    -0x38(%rbp),%rax
   0x00007ffff7343486 <+742>:   lea    0x8(%rbp,%rcx,1),%rcx
   0x00007ffff734348b <+747>:   mov    -0x30(%rbp),%rdx
   0x00007ffff734348f <+751>:   mov    -0x28(%rbp),%rbx
   0x00007ffff7343493 <+755>:   mov    -0x20(%rbp),%r12
   0x00007ffff7343497 <+759>:   mov    -0x18(%rbp),%r13
   0x00007ffff734349b <+763>:   mov    -0x10(%rbp),%r14
   0x00007ffff734349f <+767>:   mov    -0x8(%rbp),%r15
   0x00007ffff73434a3 <+771>:   mov    0x0(%rbp),%rbp
   0x00007ffff73434a7 <+775>:   mov    %rcx,%rsp
   0x00007ffff73434aa <+778>:   ret    
End of assembler dump.
(gdb) 

答案

另一个问答解释了这个问题

But in x86-64 System V, the designers chose registers from scratch, and (as my answer on that linked question shows) found that using RDI and RSI for the first 2 args saved instructions (when building SPECint with an early x86-64 port of gcc). Probably because gcc at the time liked to inline memset or memcpy using rep stosd, or the library implementation used that.

大致来说:32bits的sysv对esi/edi的使用和64bits的sysv对于rsi/rdi的使用约定并不相同,而widows和sysv对于64bits下的rsi/rdi的约定也不相同,所以容易引起混淆。如果想当然的以为32bits中esi/edi是callee saved,扩展到64bits之后对应的rsi/rdi也是callee saved,那就有些想当然(的错误)了。

补充

从gdb的代码可以看到,intel的寄存器并不是严格按照字母顺序编码数值的

static const char *att_names64[] = {
  "%rax", "%rcx", "%rdx", "%rbx", "%rsp", "%rbp", "%rsi", "%rdi",
  "%r8", "%r9", "%r10", "%r11", "%r12", "%r13", "%r14", "%r15"
};

从下面的汇编代码也可以看到,rbx并不是第二个而是第四个寄存器。

tsecer@harry: cat gcc_inline_push_reg.c 
void foo()
{
    __asm__(
            "push %rax\n\t"
            "push %rbx\n\t"
            "push %rcx\n\t"
            "push %rdx\n\t"
            "push %rsp\n\t"
            "push %rbp\n\t"
            "push %rsi\n\t"
            "push %rdi\n\t"
    );
}
tsecer@harry: gcc -g -c gcc_inline_push_reg.c
tsecer@harry: gdb gcc_inline_push_reg.o -quiet
Registered pretty printers for UE classes
Registered pretty printers for UE classes
Reading symbols from gcc_inline_push_reg.o...
(gdb) disas/r foo
Dump of assembler code for function foo:
   0x0000000000000000 <+0>:     55      push   %rbp
   0x0000000000000001 <+1>:     48 89 e5        mov    %rsp,%rbp
   0x0000000000000004 <+4>:     50      push   %rax
   0x0000000000000005 <+5>:     53      push   %rbx
   0x0000000000000006 <+6>:     51      push   %rcx
   0x0000000000000007 <+7>:     52      push   %rdx
   0x0000000000000008 <+8>:     54      push   %rsp
   0x0000000000000009 <+9>:     55      push   %rbp
   0x000000000000000a <+10>:    56      push   %rsi
   0x000000000000000b <+11>:    57      push   %rdi
   0x000000000000000c <+12>:    90      nop
   0x000000000000000d <+13>:    5d      pop    %rbp
   0x000000000000000e <+14>:    c3      ret    
End of assembler dump.
(gdb) 

那为什么Ax到Dx不是按照字母顺序编码为0——3呢?从这些讨论可以知道:或许可以认为AX到Dx只是一种巧合的注记表示方法,它们分别是Accumulate、Base、Counter、Double(和Accumulate一起组成更长的一个数值)的缩写,或许从逻辑上(或者386发布时主要是用的汇编语言来看)理解,Accumulate或许和Counter更长在一起使用?

SE的这个讨论帖子中,又更多的深入讨论,其中一个观点就是根据使用频率对寄存器进行的数值编码:

i always learned these registers as accumulate, count, data, and base. They weren't ordered alphabetically so much as ordered by usage, ax for most arithmetic operations, cx for loop counters, dx for either left over arithmetic (think of the remainder or carry for div/mul) or i/o data, and bx for a base pointer to memory. Roughly, the ACDB is the order of importance for your average use case – Steve Cox Dec 1, 2017 at 18:54

有人提到pusha指令的一个细节,所有寄存器入栈的顺序也是,ACDB,从侧面印证寄存器内部使用的是这种顺序的编码

The AX/CX/DX/BX order also makes an appearance in PUSHA, which suggests it might correspond to the internal register file implementation... – Stephen Kitt Dec 1, 2017 at 15:31

标签:__,return,eh,mov,rbp,push,rdi,rsi
From: https://www.cnblogs.com/tsecer/p/18052589

相关文章

  • hive-3.0.0 版本中遇到的bug 汇总
    目前公司用的hive版本是hive-3.0.0bug较多,这里汇总整理下,以备查阅(如有缺失欢迎补充)1.表单属性bucket_version不同,导致join数据异常1.1自查方式1.用spark-sql和hive的结果数据对比2.用hive引擎对比有无hive.optimize.joinreducededuplication=false参数时的结果目......
  • OmniPlan Pro mac版:简单、智能,项目管理新选择!
    OmniPlanPro是一款功能强大的项目管理软件,它以其直观的用户界面和丰富的功能,帮助用户轻松管理各种复杂的项目。无论是个人任务还是团队协作,OmniPlanPro都能提供全面的解决方案,让项目管理变得更加简单高效。→→↓↓载OmniPlanPro首先,OmniPlanPro拥有强大的任务管理功能。用......
  • ROS 动态参数服务器教程
    ROS动态参数服务器ros动态参数在官方叫做dynamic_reconfigure,这个功能的作用是用于node运行时修改内部参数,区别于静态读取本地yaml文件参数的方式(更常用),请见另一github仓库。主要用途是在调试机器人时能动态修改机器人参数,而不需要重新编译。具体操作流程:1、创建一个cfg文件(p......
  • Go - how the GC works
       ......
  • C++ 简易STL 教程 与 C++ 标准库
    C++STL(标准模板库)是一套功能强大的C++模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈。C++标准模板库的核心包括以下三个组件:组件描述容器(Containers)容器是用来管理某一类对象的集合。C++提供了各种不......
  • 菜刀、冰蝎、蚁剑、哥斯拉Webshell流量特征
    1.冰蝎流量特征:Accept有自己专属的流量特征,查看content-Length长度。对上传参数base64进行解码,再代码末端再次解码。可看上传流量具体操作。再response也可查看揭秘参数后的流量 2.哥斯拉流量特征:content-Length:数据包长度达万个Cookie流量包后面存在分号;  3.菜刀......
  • Linux下挂在samba 文件夹到本地
    在Linux中,可以使用mount命令来打开SMB(ServerMessageBlock)共享。sudomount-tcifs//服务器IP地址/共享目录路径/本地挂载点-ousername=用户名,password=密码其中,需要将"//服务器IP地址/共享目录路径"替换为实际的SMB共享路径;"/本地挂载点"替换为希望在本地创建的文件夹作......
  • C++U5-第06课-广度优先搜索3
    温故知新广搜的概念,编程实现基本流程 二进制矩阵中的最短路径]    【题意分析】找到一个从(0,0)到达(n-1,n-1)的路径并且路径上每一个数字都为0【思路分析】首先如果grid[0][0]=1,那么显然不存在最短路径,因此输出-1。使用dist[x][y]保存左上角单......
  • pycharm命令行进行创建虚拟环境
    安装两个模块,命令如下:pip3installvirtualenvpip3installvirtualenvwrapper-win配置环境变量选择我的电脑=>右键:选择属性=>高级系统设置=>环境变量=>系统变量=>点击新建=>填入变量名与值变量名:WORKON_HOME变量值:自定义存放虚拟环境的绝对路径WORK......
  • 【C++】【OpenCV-4.9.0】灰度图取反(Mat属性的使用)
    此次我们将一张图像转灰度后再进行灰度取反,即黑的变白的,白的变黑的,所以我们需要获取每个像素点上的灰度级,cv中提供了一个函数at,但是这个函数还有11个重载函数,太多了,我们只用这次需要用到的,即通过读取像素点的位置来获取灰度级。◆ at() [3/12]template<typename_Tp>c......