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
源码和反汇编文件如下:
在常量的定义时,首先将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、如何计算函数的跳转地址
当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。
这个ra同时代表着当前pc所在的地址,通过当前地址的偏移,可以跳转到swap函数中。由于ra地址为16进制,但是偏移的值为十进制,所以在计算的时候需要进行转化,计算后得到地址为103e,正好是swap函数所在的第一行。
Jalr的作用是跳转并寄存器链接,在完成跳转至swap函数时,ra的值为当前pc的值+4,变成了0x10ba已经指向的是printf。
进入到函数swap之后,第一步就是先保存ra返回地址,将ra保存到栈中,当程序运行结束后,将ra从栈中取出,然后执行ret,就可以跳转返回至main函数
ret是一条伪指令,实际会被扩展至jalr x0,0(x1),x1即ra寄存器,ret的作用就是不保存当前pc地址,因为x0寄存器始终为0,然后直接跳转至ra寄存器保存的地址。如此一来,函数继续往下执行。
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 // 整个函数退出
大致的流程图如下:
分析:
- 当调用不涉及到指针时,栈帧的管理相对来说比较单一,只需要考虑到入参,出参的即可,但是涉及到指针管理的时候,栈中有的时候不仅存放的是变量,还有可能存放的是地址
- main函数首先依然是将int类型的变量四字节四字节存放在栈中,但是在传递给swap函数的不是常数,而是一个地址,第一个a0为sp-28,a1为sp-32,因为sp本身栈指针,所以参数a0, a1也是指针。由于此时a0, a1为地址,所以保存至栈中时为8个字节。也就是说我们所谓的地址,取的是栈中的地址,这个栈中的地址指向的四字节内容是int常量。
- 在swap函数中,保存的是地址,这两个地址偏移四个字节的内容分别为两个常量,101和102。第一步为int c = *a,所以对应的汇编指令第一步为取栈中的地址,接着使用lw a0,0(a0)取出该地址中表示的常量,将该常量存放在栈中另外一块空间中(s0-32 ~ s0-36)中。
- 接着需要将a地址中存放的值变成b中的值,取栈中存放的第二个入参,使用lw指令取出该地址存放的常量202,接着再从栈中取a的地址,将常量b写入到a表示的地址中去。在上述汇编中,但凡是ld(load doubleworld)命令,均是对地址进行操作,因为地址需要占据八个字节,而lw是对int的常量进行操作,因为int常量占据的是四个字节。
- 最后是先取之前保存的c的值,然后取出b表示的地址,将该值赋值给该地址表示的值。
- 退出swap函数后,main栈中的值已经被换了顺序,取栈中值再调用printf即完成了函数
- 汇编中多次保存了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的优化主要体现在栈的使用是极大程度的简化了:
- 减少堆栈的溢出保护;
- swap的实现不再是依赖栈而是通过与c语言相似的方法实现直接操作寄存器;
-
在栈的使用时,不再使用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的效果。