基于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 

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 
: 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函数中执行 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 10c6: 00000097 auipc ra,0x0 10ca: 07a080e7 jalr 122(ra) # 1140 // 跳转到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 10da: fca43423 sd a0,-56(s0) 10de: 8532 mv a0,a2 10e0: 00000097 auipc ra,0x0 10e4: 060080e7 jalr 96(ra) # 1140 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 
: 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 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 106e: 00000097 auipc ra,0x0 1072: 052080e7 jalr 82(ra) # 10c0 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 1080: 00000097 auipc ra,0x0 1084: 040080e7 jalr 64(ra) # 10c0 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 
    104a:   0ca00593                li  a1,202
    104e:   00000097                auipc   ra,0x0
    1052:   062080e7                jalr    98(ra) # 10b0 

0000000000001056 <.LBB1_2>:
    printf("b = %d\n", b);
    1056:   fffff517                auipc   a0,0xfffff
    105a:   3b250513                addi    a0,a0,946 # 408 
    105e:   06500593                li  a1,101
    1062:   00000097                auipc   ra,0x0
    1066:   04e080e7                jalr    78(ra) # 10b0 
    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的效果。

你可能感兴趣的:(基于riscv架构的函数调用时栈帧管理)