前期信息
- 编译生成.elf文件以后,反汇编
arm-eabi-objdump -D ***.elf > ***.dis
可以生成汇编文件
2. 查看.dis汇编文件,例如
可以看到最前面一列为程序地址
使用
$ arm-eabi-addr2line -Cfe ***.elf 080400b0
dump_r7_callback
/media/data/MTK/MT7682/mt7682/sdk/project/mt7682_hdk/apps/lock/GCC/../../../../../project/mt7682_hdk/apps/lock/../common/src/arm_backtrace.c:102
就可以得到对应的代码位置.
所以如果要回溯堆栈,那么就要能得到类似080400b0这样的地址堆栈
然后使用addr2line来回溯到sourcecode的堆栈
编译命令
编译cortex-m4的thumb代码
arm-none-eabi-gcc -mthumb -mcpu=cortex-m4 -c -o main.o main.c
编译arm代码
arm-none-eabi-gcc -c -o main.o main.c
疑问
arm模式编译的代码:
arm-none-eabi-gcc -fno-omit-frame-pointer -c -o main.o main.c
arm-none-eabi-objdump -d main.o
0000001c <func1>:
1c: e92d4800 push {fp, lr} 压栈 fp/lr, sp会自动减8
20: e28db004 add fp, sp, #4 将fp回退4,指向堆栈里面的fp
24: e24dd008 sub sp, sp, #8
28: e50b0008 str r0, [fp, #-8]
2c: e50b100c str r1, [fp, #-12]
30: ebfffffe bl 0 <func2>
34: e1a03000 mov r3, r0
38: e1a00003 mov r0, r3
3c: e24bd004 sub sp, fp, #4
40: e8bd8800 pop {fp, pc}
thumb模式编译的代码:
00000010 <func1>:
10: b580 push {r7, lr} 得出r7作为frame pointer的结论
12: b082 sub sp, #8
14: af00 add r7, sp, #0 ???? 为什么会把修改过的sp赋值给r7,指向栈底?
16: 6078 str r0, [r7, #4]
18: 6039 str r1, [r7, #0]
1a: f7ff fffe bl 0 <func2>
1e: 4603 mov r3, r0
20: 4618 mov r0, r3
22: f107 0708 add.w r7, r7, #8
26: 46bd mov sp, r7
28: bd80 pop {r7, pc}
2a: bf00 nop
以上对比,
arm模式下 r11(fp)作为frame pointer
thumb模式下 r7 作为frame pointer
但是区别是arm的fp指向当前栈帧的栈底的fp,thumb的指向栈顶,这个怎么回溯堆栈?
测试
1 栈类型
uint32_t*sp;
__asm volatile ( " MOV r0, #12\n " );
__asm volatile ( " MOV r1, #13\n " );
__asm volatile ( " MOV r2, #14\n " );
__asm volatile ( " PUSH {r0, r1, r2}\n " );
asm volatile ("mov %0, sp" : "=r" (sp));
printf( " 0 %08x\r\n " , sp[ 0 ]);
printf( " 4 %08x\r\n " , sp[ 1 ]);
printf( " 8 %08x\r\n " , sp[ 2 ]);
以上代码可以验证堆栈类型,输出
0 0000000c
4 0000000d
8 0000000e
得出的结论:
1 递减堆栈,否则sp[0] [1] [2] 不会为压栈的数据
2 满减堆栈,参考下图,即SP移动以后,是指向最后一个压栈的有效数据
3 push {r0, r1, r2}的入栈顺序为:r2->r1->r0,因为r2在最高地址
可以使用下列代码来打印感兴趣的寄存器信息
void dump_r7_callback()
{
uint32_t* fp, *fp2, *pc;
asm volatile ("mov %0, r7" : "=r" (fp));
asm volatile ("mov %0, r11" : "=r" (fp2));
asm volatile ("mov %0, pc" : "=r" (pc));
printf("dump_r7_callback fp 0x%x addr %p fp2 0x%x %p pc 0x%x %p\n",
fp, &fp, fp2, &fp2, pc, &pc);
}
实现方法
参考上图,既然r7指向栈尾,并且子函数开始的时候,会将lr/r7先后入栈
那么我们可以从当前的sp地址开始回溯,直到当前task栈的最顶部
如果有一个变量的内容比存放他的地址大8,则此地址存放的是r7,往上4个字节为lr,例如:
//向下生长的堆栈
int dump_backtrack_by_scan_stack(uint32_t *stackEnd, uint32_t *stackStart)
{
//回溯整个堆栈
printf("dump_backtrack_by_scan_stack stack %p - %p\n", stackStart, stackEnd);
int depth = 0;
for(uint32_t* p = stackEnd; p <= stackStart; p ++)
{
uint32_t sp_data = *p;
if((sp_data - (uint32_t)p) == 8) {
uint32_t lr = *(p + 1) - 4;
if(lr >= code_start && lr <= code_end) {
printf(" #%d: %8lx\n", depth++, *(p + 1) - 4);
// printf("dump_current_thread: r7 addr %p data 0x%lx, lr 0x%lx\n",
// p, sp_data, *(p + 1) - 4);
}
}
}
return 0;
}
这样我们就可以回溯出来当前task对应的调用栈。
backtrace其他堆栈
当前task的堆栈与其他task不同的地方在于,无法直接获取到sp,也就是栈底的位置。
注意事项
-
ARM Cortex-A用的是r11做frame pointer,thumb系列的用可能是r7,这个可以查看加上-no-omit-frame-pointer选项以后,编译出来的汇编文件压栈的是哪个寄存器
-
如果反汇编出来的寄存器与你希望的不同,比如我们希望r7来做fp寄存器,但是汇编代码中r7总是被修改,那么要考虑编译的优化选项当中,是否有Ox的优化选项,可以去掉。
注意一定要看所有的编译选项,是否有Os,因为编译选项可能很长,会漏掉一些地方 -
注意编译完,多反汇编几次,查看你需要的寄存器有没有被意外使用
例如,当我们想打印r7的时候,r7之前已经被重新赋值了,所以就不会是我们需要的fp寄存器了。
reference:
下图可以作为参考,但是不同的CPU的栈帧结构不完全一样,而且编译选项不同,栈帧结构也会 不同
https://github.com/JnuSimba/AndroidSecNotes/blob/master/Android%E9%80%86%E5%90%91%E5%9F%BA%E7%A1%80/ARM%20%E5%AF%84%E5%AD%98%E5%99%A8%E7%AE%80%E4%BB%8B.md
https://developer.arm.com/documentation/100067/0607/armclang-Command-line-Options/-fomit-frame-pointer—fno-omit-frame-pointer