基于riscv架构的函数调用时栈帧管理

1.分析工具——反汇编

在一开始的时候由于我在bionic中做过类似的事情,就图方便直接在bionic中对hello.c文件进行了修改,调整了bp参数并把编译好的目标文件objdump成汇编文件进行查看。
本篇主要分析在函数调用时的栈帧管理,首先是利用objdump对c文件进行反汇编。基于bionic中的hello.c文件进行编译并进行反汇编,由于在编译bionic时,默认编译的是静态链接,反汇编出来的文件非常多,所以需要在Android.bp/home/user/codes/zhimo-aosp-d_art/bionic/libc/Android.bp中对hello.c的编译属性进行修改,修改如下:

cc_binary {
    name: "Hello",
    srcs: ["hello.c"],
    arch:{
        riscv64:{
            cflags: [
                "-Wno-error=implicit-function-declaration","-g", "-O0"  // 修改编译优化程度为O0,尽量减少优化
                    ],
            ldflags: [
                "-Wl,-verbose",
                    ],
        // static_libs: [
        //         "libc",
        //     ],                                // 删除静态的依赖库
            // static_executable: true,    // 删除静态标志
                }
            }
        }

修改完成之后hello的编译不再依赖静态库,mmm bionic之后,在主分支下用反汇编命令生成汇编文件,命令如下:./prebuilts/./gcc/linux-x86/riscv64/riscv64-linux-android-9.2.0/riscv64-elf/bin/objdump -S out/target/product/eswin_riscv64/symbols/system/bin/Hello > Hello.S
在安装框架下编译c文件,使用的编译工具为clang,但是反汇编使用的工具上述变成了gcc,于是我们尝试使用llvm来反汇编:
./prebuilts/clang/host/linux-x86/clang-dev/bin/llvm-objdump -S out/target/product/eswin_riscv64/symbols/system/bin/Hello > Hello.S

但是我们也可以不依赖于bionic,因为bp使用的编译器为clang,我们也可以使用gcc 对任意一个c文件进行编译,生成目标文件之后反汇编成汇编文件。
./prebuilts/./gcc/linux-x86/riscv64/riscv64-linux-android-9.2.0/bin/riscv64-linux-android-gcc hello.c -Wno-error=implicit-function-declaration -g -O0 -o Hello
./prebuilts/./gcc/linux-x86/riscv64/riscv64-linux-android-9.2.0/riscv64-elf/bin/objdump -S Hello > Hello.S

2、一个简单的例子——add

源码和反汇编文件如下:

add

在常量的定义时,首先将int类型的常量101赋值给a1寄存器,然后保存至栈的s0-20 ~ s0-24中,因为int类型的常量占据四个字节,所以此处在栈中占据的空间也是四个字节。在定义另外一个常量b = 202时,还是将常量202赋值给a1寄存器,然后保存至栈的s0-24 ~ s0-28中。

由于涉及调用到了add函数,add函数有两个参数需要传参,需要把值101和202作为两个入参传入,在riscv中,入参寄存器默认为a0, a1……等以此类推。所以此处将栈中的值取出,分别赋值给a0, a1。

在add函数中,首先是在原来的地址基础上,继续开辟一块栈空间为32字节。第一步是保存返回地址,当add函数运行结束的时候跳转回原来调用的地方,并保存s0帧指针,当调用结束时,帧指针仍然执行调用之前的地址。在把常量保存至栈中后,取栈中的值进行加操作后,把结果赋值给a0作为返回寄存器,带着add函数的返回值返回。

3、如何计算函数的跳转地址

swap

当main中调用swap函数时对应的跳转如上图所示。

10b2: 97 00 00 00        auipc   ra, 0     // ra = 10b2
10b6: e7 80 c0 f8        jalr    -116(ra)   // 0x10b2 – 116 = 103e

auipc的作用是将立即数左移12位,然后取这个立即数的31-12的高地址,低地址设为0,将这个地址赋值给ra。


auipc

这个ra同时代表着当前pc所在的地址,通过当前地址的偏移,可以跳转到swap函数中。由于ra地址为16进制,但是偏移的值为十进制,所以在计算的时候需要进行转化,计算后得到地址为103e,正好是swap函数所在的第一行。


swap

Jalr的作用是跳转并寄存器链接,在完成跳转至swap函数时,ra的值为当前pc的值+4,变成了0x10ba已经指向的是printf。
jalr

进入到函数swap之后,第一步就是先保存ra返回地址,将ra保存到栈中,当程序运行结束后,将ra从栈中取出,然后执行ret,就可以跳转返回至main函数


swap

ret是一条伪指令,实际会被扩展至jalr x0,0(x1),x1即ra寄存器,ret的作用就是不保存当前pc地址,因为x0寄存器始终为0,然后直接跳转至ra寄存器保存的地址。如此一来,函数继续往下执行。
ret

4、涉及到指针的栈帧管理

先来看例子,实现的是a与b交换的功能

#include <stdio.h>

void swap(int *a, int *b){
    int c = *a;
    *a = *b;
    *b = c;
}


int main()
{
    int a= 101;
    int b= 202;
    swap(&a, &b);
    printf("a = %d\n", a);
    printf("b = %d\n", b);
    return 0;
}

对应的汇编代码如下(删除了print,栈溢出检查等无关部分):

void swap(int *a, int *b){
    103e:   7179                    addi    sp,sp,-48       // 继续给栈开辟48字节的空间
    1040:   f406                    sd  ra,40(sp)           // 将ra保存至栈中
    1042:   f022                    sd  s0,32(sp)           // 将s0保存至栈中
    1044:   1800                    addi    s0,sp,48        // s0此时只想栈的高地址
    1046:   fea43423                sd  a0,-24(s0)          // 将a0寄存器的地址,到s0-16 ~ s0-24中
    104a:   feb43023                sd  a1,-32(s0)          // 将a1寄存器的地址,保存到s0-24 ~ s0-32中   
    int c = *a;
    104e:   fe843503                ld  a0,-24(s0)          // 从s0-16 ~ s0-24中读取八个字节到a0中,该地址为a0传入的地址,地址中存储的值为101
    1052:   4108                    lw  a0,0(a0)            // 将a0地址中的值加载到a0寄存器中,此时a0为101
    1054:   fca42e23                sw  a0,-36(s0)          // 将a0的值加载到s0-32 ~ s0-36中,此处存入的为一个值
    *a = *b;
    1058:   fe043503                ld  a0,-32(s0)          // 将s0-24 ~ s0-32的地址读取到a0中
    105c:   4108                    lw  a0,0(a0)            // 将a0的值加载给a0,此时a0为202
    105e:   fe843583                ld  a1,-24(s0)          // 将s0-16 ~ s0-24的八字节地址给a1
    1062:   c188                    sw  a0,0(a1)            // 将a0的值加载到a1的地址中,此时a1的地址,即s0-16 ~ s0-24中的值为202
    *b = c;
    1064:   fdc42503                lw  a0,-36(s0)          // 将s0-32 ~ s0-36存的值加载给a0,此处的值为101
    1068:   fe043583                ld  a1,-32(s0)          // 将s0-24 ~ s0-32的八字节地址读取到a1中
    106c:   c188                    sw  a0,0(a1)            // 将a0的值读取到a1中,原来s0-24 ~ s0-32的地址的值变成了101

}
    106e:   7402                    ld  s0,32(sp)           // 恢复s0
    1070:   70a2                    ld  ra,40(sp)           // 恢复ra
    1072:   6145                    addi    sp,sp,48        // 恢复sp指针
    1074:   8082                    ret                     // 退出

0000000000001076 <main>:


int main()
{
    1076:   7139                    addi    sp,sp,-64       // 给栈开辟64字节的,此处sp为栈指针
    1078:   fc06                    sd  ra,56(sp)           // 将返回地址ra保存到栈中
    107a:   f822                    sd  s0,48(sp)           // 将s0(fp)帧指针保存到栈中
    107c:   0080                    addi    s0,sp,64        // sp指向的是栈底,s0此时的值为sp+64的值,也就是基地址

000000000000107e <.LBB1_3>:
    107e:   00001517                auipc   a0,0x1          // 将0x1取31位到12位,然后左移12位+pc的地址,结果写入到a0寄存器
    1082:   19a53503                ld  a0,410(a0) # 2218 <__stack_chk_guard@LIBC>  // 将a0偏移410字节的值给a0,此步骤是为了防止堆栈溢出添加的检测保护       
    1086:   610c                    ld  a1,0(a0)            // 将a0的八字节赋给a1
    1088:   feb43423                sd  a1,-24(s0)          // 将a1寄存器的值保存到栈中,保存至s0-16 ~ s0-24的位置
    108c:   4581                    li  a1,0                // 将a1寄存器赋值为0,此处的作用为初始化一个寄存器为0
    108e:   fcb42e23                sw  a1,-36(s0)          // 将a1寄存器的值保存到栈中表示的地址(s0-32 ~ s0-36的位置)
    1092:   06500593                li  a1,101              // 将a1寄存器赋值为101
    int a= 101; 
    1096:   feb42223                sw  a1,-28(s0)          // 由于int在类型为四个字节,此时将101存储到s0-24 ~ s0-28所表示的地址中
    109a:   0ca00593                li  a1,202              // 还是将a0寄存器进行操作,赋值为202
    int b= 202;
    109e:   feb42023                sw  a1,-32(s0)          // 将202存储到s0-28 ~ s0-32的地址中
    10a2:   fe440593                addi    a1,s0,-28       // 将s0-28的地址给到a1
    10a6:   fe040613                addi    a2,s0,-32       // 将s0-32中地址给到a2
    swap(&a, &b);
    10aa:   fca43823                sd  a0,-48(s0)          // 将a0的地址保存到s0-40 ~ s0-48中
    10ae:   852e                    mv  a0,a1               // 将a1复制给a0
    10b0:   85b2                    mv  a1,a2               // 将a2复制给a1
    10b2:   00000097                auipc   ra,0x0          
    10b6:   f8c080e7                jalr    -116(ra) # 103e <swap> // 跳转到swap函数中执行
    printf("a= %d\n", a);
    10ba:   fe442583                lw  a1,-28(s0)          //将s0-24 ~ s0-28的地址存入到a1寄存器中

00000000000010be <.LBB1_4>:
    10be:   fffff517                auipc   a0,0xfffff
    10c2:   3da50513                addi    a0,a0,986 # 498 <abitag+0x1d8>
    10c6:   00000097                auipc   ra,0x0
    10ca:   07a080e7                jalr    122(ra) # 1140 <printf@plt> // 跳转到printf中实现打印
    printf("b= %d\n", b);
    10ce:   fe042583                lw  a1,-32(s0)

00000000000010d2 <.LBB1_5>:
    10d2:   fffff617                auipc   a2,0xfffff
    10d6:   3cd60613                addi    a2,a2,973 # 49f <abitag+0x1df>
    10da:   fca43423                sd  a0,-56(s0)
    10de:   8532                    mv  a0,a2
    10e0:   00000097                auipc   ra,0x0
    10e4:   060080e7                jalr    96(ra) # 1140 <printf@plt>
    10e8:   fd043583                ld  a1,-48(s0)          // 将s0-40 ~ s0-48的地址赋值给a1,之前存入的是a0的地址
    10ec:   6190                    ld  a2,0(a1)            // 将a1的地址复制给a2
    10ee:   fe843683                ld  a3,-24(s0)          // 将s0-16 ~ s0-24的八字节赋给a3,也就是之前存入的a0的地址
    10f2:   00d61963                bne a2,a3,1104 <.LBB1_2>    //判断此处的栈是否溢出了
    10f6:   0040006f                j   10fa <.LBB1_1>

00000000000010fa <.LBB1_1>:
    10fa:   4501                    li  a0,0                // 将a0寄存器赋值为0
    return 0;
    10fc:   7442                    ld  s0,48(sp)           // 恢复s0的值
    10fe:   70e2                    ld  ra,56(sp)           // 恢复ra的值
    1100:   6121                    addi    sp,sp,64        // 恢复sp的值
    1102:   8082                    ret                     // 整个函数退出

大致的流程图如下:


swap函数调用时的栈

分析:

  1. 当调用不涉及到指针时,栈帧的管理相对来说比较单一,只需要考虑到入参,出参的即可,但是涉及到指针管理的时候,栈中有的时候不仅存放的是变量,还有可能存放的是地址
  2. main函数首先依然是将int类型的变量四字节四字节存放在栈中,但是在传递给swap函数的不是常数,而是一个地址,第一个a0为sp-28,a1为sp-32,因为sp本身栈指针,所以参数a0, a1也是指针。由于此时a0, a1为地址,所以保存至栈中时为8个字节。也就是说我们所谓的地址,取的是栈中的地址,这个栈中的地址指向的四字节内容是int常量。
  3. 在swap函数中,保存的是地址,这两个地址偏移四个字节的内容分别为两个常量,101和102。第一步为int c = *a,所以对应的汇编指令第一步为取栈中的地址,接着使用lw a0,0(a0)取出该地址中表示的常量,将该常量存放在栈中另外一块空间中(s0-32 ~ s0-36)中。
  4. 接着需要将a地址中存放的值变成b中的值,取栈中存放的第二个入参,使用lw指令取出该地址存放的常量202,接着再从栈中取a的地址,将常量b写入到a表示的地址中去。在上述汇编中,但凡是ld(load doubleworld)命令,均是对地址进行操作,因为地址需要占据八个字节,而lw是对int的常量进行操作,因为int常量占据的是四个字节。
  5. 最后是先取之前保存的c的值,然后取出b表示的地址,将该值赋值给该地址表示的值。
  6. 退出swap函数后,main栈中的值已经被换了顺序,取栈中值再调用printf即完成了函数
  7. 汇编中多次保存了a0,是在函数退出的时候进展栈溢出检查,如果相等说明并没有溢出

5. 不同的编译优化模式下的汇编

5.1 使用-O1优化

以上展示了在使用-O0的情况下函数调用中思路以及栈的情况,但是如果使用了不同的编译优化方法,情况并不相同。以下是使用-O1的优化方式:

void swap(int *a, int *b){
    int c = *a;
    103e:   4110                    lw  a2,0(a0)        
    //  不再使用栈来交换,而是之间将传入的a0,a1寄存器通过与c语言一样的方式进行交换,交换完成直接ret返回跳转至print
    *a = *b;
    1040:   4194                    lw  a3,0(a1)
    1042:   c114                    sw  a3,0(a0)
    *b = c;
    1044:   c190                    sw  a2,0(a1)
}
    1046:   8082                    ret

0000000000001048 <main>:

int main()
{
    1048:   1141                    addi    sp,sp,-16
    // 此处栈开辟的空间只有16字节,相比于-O0减少了堆栈的溢出检查,也取消了对s0寄存器的使用,栈的寻址都是基于sp
    104a:   e406                    sd  ra,8(sp)
    104c:   06500513                li  a0,101
    int a= 101;
    1050:   c22a                    sw  a0,4(sp)
    1052:   0ca00513                li  a0,202
    int b= 202;
    1056:   c02a                    sw  a0,0(sp)
    1058:   0048                    addi    a0,sp,4
    105a:   858a                    mv  a1,sp
    swap(&a, &b);
    105c:   00000097                auipc   ra,0x0
    1060:   fe2080e7                jalr    -30(ra) # 103e <swap>
    printf("a = %d\n", a);
    1064:   4592                    lw  a1,4(sp)

0000000000001066 <.LBB1_1>:
    1066:   fffff517                auipc   a0,0xfffff
    106a:   3aa50513                addi    a0,a0,938 # 410 <abitag+0x150>
    106e:   00000097                auipc   ra,0x0
    1072:   052080e7                jalr    82(ra) # 10c0 <printf@plt>
    printf("b = %d\n", b);
    1076:   4582                    lw  a1,0(sp)

0000000000001078 <.LBB1_2>:
    1078:   fffff517                auipc   a0,0xfffff
    107c:   39050513                addi    a0,a0,912 # 408 <abitag+0x148>
    1080:   00000097                auipc   ra,0x0
    1084:   040080e7                jalr    64(ra) # 10c0 <printf@plt>
    return 0;
    1088:   4501                    li  a0,0
    108a:   60a2                    ld  ra,8(sp)
    108c:   0141                    addi    sp,sp,16
    108e:   8082                    ret

相比于-O0的编译方式-O1的优化主要体现在栈的使用是极大程度的简化了:

  1. 减少堆栈的溢出保护;
  2. swap的实现不再是依赖栈而是通过与c语言相似的方法实现直接操作寄存器;
  3. 在栈的使用时,不再使用s0,在栈中寻址主要依赖sp;


    -O1

5.2 使用-O2优化

反汇编结果如下:

int main()
{
    103e:   1141                    addi    sp,sp,-16
    1040:   e406                    sd  ra,8(sp)

0000000000001042 <.LBB1_1>:
    int a= 101;
    int b= 202;
    swap(&a, &b);
    printf("a = %d\n", a);
    1042:   fffff517                auipc   a0,0xfffff
    1046:   3ce50513                addi    a0,a0,974 # 410 <abitag+0x150>
    104a:   0ca00593                li  a1,202
    104e:   00000097                auipc   ra,0x0
    1052:   062080e7                jalr    98(ra) # 10b0 <printf@plt>

0000000000001056 <.LBB1_2>:
    printf("b = %d\n", b);
    1056:   fffff517                auipc   a0,0xfffff
    105a:   3b250513                addi    a0,a0,946 # 408 <abitag+0x148>
    105e:   06500593                li  a1,101
    1062:   00000097                auipc   ra,0x0
    1066:   04e080e7                jalr    78(ra) # 10b0 <printf@plt>
    return 0;   
    106a:   4501                    li  a0,0
    106c:   60a2                    ld  ra,8(sp)
    106e:   0141                    addi    sp,sp,16
    1070:   8082                    ret

可以看出,使用-O2优化编译的差别比较大,直接优化了swap,直接将a1先赋值为202打印,再将a1赋值成101,打印。其结果就达到了swap的效果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 堆栈在计算机程序运行时发挥强大的作用,能用于临时存储大量的数据,也便于数据的查找。作为一名编程爱好者,小编大学时...
    ITsCLG阅读 2,415评论 0 2
  • 零. 课程要点: C语言内存模型 函数调用的机器级表示 从这一章开始,我们要运用之前所学的计算机系统基础知识,来理...
    KPlayer阅读 1,199评论 0 1
  • 在高级语言中,函数调用很简单,直接调用并传入相关的参数即可。在汇编语言中除了传参外,还要有当前数据入栈、申请新函数...
    秦砖阅读 4,899评论 1 5
  • 本文以Linux + arm64平台上的测试程序为例,讲解函数调用的栈帧回溯基本原理。 1. Overview 相...
    dumphex阅读 5,553评论 0 0
  • 栈: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函...
    zjfclimin阅读 4,110评论 0 5