标签:映射 读写 机器 技术 优化 执行 嵌套 x86 测试
这篇主要是杭州操作系统大会前辈的文档进行学习,因为文档公开了故而总结学习一下,如若其中有侵权的地方,请及时联系我,谢谢
...........................................................................................................................................................................................................................
一台客户现场机器,运行一周左右偶然发生一次应用段错误或者double free问题,cpu可能是arm、mips、x86等架构,有什么好的方法捕捉异常日志?
1. 研发环境常使用gdb+coredump技术解决此类问题,客户现场等非研发环境的偶现应用异常问题,不方便使用,操作起来有一定难度
2. 不同架构(arm32、arm64、mips、x86),不同版本C库和gdb,栈回溯效果差异很大。PC ubuntu系统测试,glibc 2.15,发生应用double free,直接打印栈回溯信息,其他架构的CPU上测试没有这个功能。arm64架构的某款CPU上测试,gdb对strip过的应用程序无法栈回溯, PC ubuntu系统测试没有这个问题。
当执行入栈操作时,lr和fp寄存器的值存入栈中,然后令fp寄存器指向函数栈的栈顶,本例是函数栈第二片内存地址(函数无局部变量)。栈回溯时,首先根据fp寄存器指向的地址,取出保存在函数栈中lr和fp寄存器的数据,lr的值是函数返回地址,fp的值是上一级函数栈的栈顶地址
1.堆栈指针r13(SP):每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),
都有各自独立的堆栈,用不同的堆栈指针来索引。这样当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性 2.连接寄存器r14(LR):每种模式下r14都有自身版组,它有两个特殊功能 (1)保存子程序返回地址。使用BL或BLX时,跳转指令自动把返回地址放入r14中;子程序通过把r14复制到PC来实现返回 (2)当异常发生时,异常模式的r14用来保存异常返回地址,将r14如栈可以处理嵌套中断。 3、程序计数器r15(PC):PC是有读写限制的。当
没有超过读取限制的时候,读取的值是指令的地址加上8个字节,由于ARM指令总是以字对齐的,故bit[1:0]总是00。
当用str或stm存储PC的时候,偏移量有可能是8或12等其它值
函数unwind_frame中: frame->pc = *(unsigned long *)(fp + 8)计算上一级函数指令地址,也就是当前函数的返回地址。 frame->fp = *(unsigned long *)(fp)计算上一级函数栈的栈顶地址
假设应用程序函数执行流程是test_c()->test_b()->test_a(),test_a()函数发生段错误,内核将自动执行do_page_fault(……,struct pt_regs *regs)函数,该结构体中regs->pc是发生段错误test_a()函数的指令地址,假如是0x400538,regs->regs[29]就是fp寄存器。怎么实现内核对段错误应用的栈回溯?
模仿unwind_frame函数,增加user_unwind_frame函数,以实现do_page_fault函数中,对段错误应用程序栈回溯,代码如图。经过栈回溯,假设从test_a函数栈中分析出test_a函数返回地址是0x400550(处于test_b函数中),继续栈回溯,找到test_b函数的返回地址是0x400588(处于test_c函数)
这样可以在内核do_page_fault中,对段错误应用程序栈回溯,执行过程打印如下: user thread backstrace pc1:0x400538 pc2:0x400550 pc3:0x400588
反汇编后可以知道函数调用流程是test_c()->test_b()->test_a()。这个方法还可以继续优化:还是do_page_fault函数中,对应用栈回溯过程,读取可执行程序elf文件信息,分析并打印出该指令地址所在的函数的名字。这需要用到elf可执行程序文件数据分布的原理,尤其是elf文件 section部分的数据
在内核里读取elf可执行程序文件的“.symtab”和”.strtab” section的数据,就可以分析出该文件的test_c()、test_b()、test_a()三个函数名字字符串、函数运行首地址、函数指令字节数。比如数据如下
函数名字 函数指令首地址 函数指令结束地址 test_c 0x400518 0x400518 +0x27 test_b 0x400545 0x400545 +0x35
user thread backstrace [<0x400538>] test_a + 0x20/0x27 [<0x400550>] test_b +0x0b/0x35 [<0x400588>] test_c + 0x08 /0x20
#include <stdio.h> #include <stdlib.h> char buf[5]; int test_a() { printf("%s \n", __func__); memcpy(buf, "12345677", 7); return 0; } int test_b() { printf("%s \n", __func__); memset(buf, 0, sizeof(buf)); test_a(); return 0; } int test_c() { printf("%s \n", __func__); sleep(1); test_b(); return 0; } int main() { printf("%s \n", __func__); test_c(); return 0; }
例子是一个可执行程序test演示代码,用到了memcpy等库函数,本例是C库文件libc.so中的函数
可执行程序文件的“.dynstr” section包含了用到的库函数名字,” .dynsym” section的数据是一个个struct elf64_sym结构体,每个对应一个用到的库函数结构体。两个section表述的库函数信息是一一对应的,如下图:
ibc.so库文件的“.dynstr” section包含了C库所有的库函数名字,” .dynsym” section的数据也是一个个struct elf64_sym结构体,每个对应一个C库的库函数结构体
libc.so的”.dynsym” section的库函数结构体struct elf64_sym中, st_value是库函数原始首地址、st_size是库函数指令字节数。为什么是原始首地址?因为可执行程序调用C库函数时,会对C库函数进行一次重定向,然后映射到可执行程序的应用空间,最后才执行C库函数的指令代码
1 “.plt” section汇编代码 2 0000000000400480 <memcpy@plt>: 3 ………….. 4 400484: f944fa11 ldr x17, [x16,#2544] 5 400488: 9127c210 add x16, x16, #0x9f0 6 40048c: d61f0220 br x17
1 test_a函数汇编代码 2 0000000000400650 <test_a>: 3 400650: a9bf7bfd stp x29, x30, [sp,#-16]! 4 400654: 910003fd mov x29, sp 5 …………………. 6 400660: 97ffffa0 bl 4004e0 <puts@plt> 7 ……………… 8 400678: 97ffff82 bl 400480 <memcpy@plt>
如test_a函数汇编代码,当执行memcpy函数,实际是先执行“.plt” section的memcpy@plt 函数。然后在memcpy@plt函数汇编代码里,ldr x17, [x16,#2544]计算出memcpy库函数实际运行地址在“.got.plt” section的内存地址0x410a38,取出该地址的数据存于x17寄存器。如右图所示,就是把橙色内存单元的数据0x7f91db5a40保存到x17,然后br x17就是跳转到memcpy库函数实际首地址,执行该函数的代码
如果我们能知道libc.so中所有库函数的运行首地址和结束地址,这样当在C库中崩溃,比如此时pc值是0x7f91db5a60,我们就能知道0x7f91db5a60处于哪个库函数,这样就知道怎么在C库中栈回溯了。
具体实现方法:
double free是C库检测到异常,然后向当前进程发送SIGABRT信号,然后进入内核空间,会执行到do_send_specific函数发送信号。在该函数中,检测到是SIGABRT信号,通过task_pt_regs(current)获取异常进程进入内核空间前pc、lr、fp等寄存器,然后运用前文的栈回溯原理,对double free应用流程栈回溯,如下是演示效果。
应用在test_a函数调用free库函数两次后,内核打印:
[< 0x7f91dxxxx>] raise() 0x38/0x78 [< 0x7f91dxxxx>] abort() 0x1b0/0x308 [<0x000400538>] test_a() 0x6c/0xa4 [<0x000400550>] test_b() 0x20/0x458 [<0x000400588>] test_c() 0x20/0x64
标签:映射 读写 机器 技术 优化 执行 嵌套 x86 测试
原文地址:https://www.cnblogs.com/mysky007/p/12539754.html