内核栈回溯原理学习应用(代码片段)

mysky007 mysky007     2023-04-05     751

关键词:

  这篇主要是杭州操作系统大会前辈的文档进行学习,因为文档公开了故而总结学习一下,如若其中有侵权的地方,请及时联系我,谢谢

...........................................................................................................................................................................................................................

问题:

    一台客户现场机器,运行一周左右偶然发生一次应用段错误或者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 
", __func__);
    memcpy(buf, "12345677", 7);
    return 0;

int test_b()

    printf("%s 
", __func__);
    memset(buf, 0, sizeof(buf));
    test_a();
    return 0;

int test_c()

    printf("%s 
", __func__);
    sleep(1);
    test_b();
    return 0;

int main()

    printf("%s 
", __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库中栈回溯了。

具体实现方法:

  1. 以memcpy中崩溃为例, 从libc.so文件的” .dynsym” section找到memcpy库函数的struct elf64_sym结构,该结构的成员st_value就是memcpy库函数的原始首地址
  2. 从可执行程序”.got.plt” section找到库函数memcpy的运行首地址,memcpy的运行首地址减去其原始首地址就是库函数的原始首地址与运行首地址之差,命名为dx
  3. 从libc.so分离出所有库函数的struct elf64_sym结构,知道每个库函数的原始首地址,原始首地址+dx就是每个库函数的运行首地址,再结合st_size就知道库函数的运行结束地址。从libc.so文件的“.dynstr” section又知道了每个库函数的名字。这样知道了每个库函数运行首地址、结束地址、函数名字,就具备了栈回溯的条件

技术图片

 

 

double free应用:

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

 

c++栈回溯原理(代码片段)

...代码,是如何将所在线程此刻完整的函数调用堆栈给回溯进来的呢?下面我们就来讲讲栈回 查看详情

缓冲区溢出攻击

...述图主要描述进程虚拟地址空间,即用户空间内容,进程内核地址空间为所有进程共用(Linux内核中)栈区:理解为函数栈帧的存储位置,在应用层可以通过回溯栈区得到函数调用地址,即栈回溯;堆区:程序运行过程中动态分 查看详情

flink内核原理学习内存模型(代码片段)

Flink内核原理学习之内存模型文章目录Flink内核原理学习之内存模型一、JVM内存管理的缺点二、TaskManager内存模型三、内存数据结构四、网络传输中的内存管理4.1网络IO内存管理4.2反压机制Java、大数据开发学习要点(持续更新... 查看详情

flink内核原理学习任务提交流程(代码片段)

Flink内核原理学习之任务提交流程文章目录Flink内核原理学习之任务提交流程一、Flink任务提交流程(yarn-per-job模式)1.1总体流程解析1.2具体组件解释二、Flink任务提交流程(yarn-session模式)Java、大数据开发学习要点(持续更新中…... 查看详情

docker学习笔记(代码片段)

...、 原理1、没使用Docker之前,所有应用共享Lib文件和内核2、使用之后,每个容器相当于单独的Linux操作系统,仅仅共享内核,也就是可以配置多个Tomcat应用3、原理讲解二、名词三、镜像查询原理图四、基本命令1... 查看详情

flink内核原理学习组件通信rpc(代码片段)

Flink内核原理学习之RPC文章目录Flink内核原理学习之RPC一、Akka与Actor模型二、RPC消息类型三、Flink通信组件3.1RpcGateway3.2RpcEndpoint3.3RpcService与RpcServer3.4AkkaRpcActor四、PRC交互过程Java、大数据开发学习要点(持续更新中…)一、... 查看详情

栈7:单调栈的原理和应用(代码片段)

​在刷LeetCode之前,我看到过单调栈,感觉就是在栈的基础上加了个要求而已,没怎么当回事。但是在刷LeetCode题目的时候,才知道有些题用单调栈非常好使。本着学会一个,刷掉一片的原则,我们用几篇... 查看详情

关于linux和linux驱动程序解bug的技巧和思考方法

...序,异常处理程序根据发生异常的地方,决定1)如果在内核栈,则调panic进而回溯出内核栈。如果看crashdump的log时,看到回溯栈前部分在异常代码中,后部分才是发生异常的栈,(后部分可能还会出现一部分是中断处理程序的栈... 查看详情

关于linux和linux驱动程序解bug的技巧和思考方法

...序,异常处理程序根据发生异常的地方,决定1)如果在内核栈,则调panic进而回溯出内核栈。如果看crashdump的log时,看到回溯栈前部分在异常代码中,后部分才是发生异常的栈,(后部分可能还会出现一部分是中断处理程序的栈... 查看详情

linux内核的配置和编译原理(代码片段)

目录一、linux内核源码目录结构二、内核配置和编译体验三、内核的配置原理四、menuconfig的使用和演示五、menuconfig的工作原理六、Kconfig文件详解七、menuconfig的实验学习思路一、linux内核源码目录结构1、源码从哪里来(1)上篇博客... 查看详情

栈溢出学习(代码片段)

...,所有题目都在ctfwiki上可以找到。加油加油。栈溢出原理栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。看一个简单的程序:#incl... 查看详情

栈溢出学习(代码片段)

...,所有题目都在ctfwiki上可以找到。加油加油。栈溢出原理栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。看一个简单的程序:#incl... 查看详情

linux源码解析-内核栈与thread_info结构详解(代码片段)

1.什么是进程的内核栈?在内核态(比如应用进程执行系统调用)时,进程运行需要自己的堆栈信息(不是原用户空间中的栈),而是使用内核空间中的栈,这个栈就是进程的内核栈2.进程的内核栈在计算机中是如何描述的?linux... 查看详情

数据结构与算法同种算法分别用递归/回溯与栈实现(代码片段)

一、阶乘importjava.util.Stack;publicclassMain publicstaticintfact1(intn) if(n==0)return1; elsereturnn*fact1(n-1); publicstaticintfact2(intn) intans=1; Stack<Integer>stack=ne 查看详情

一起学习正则表达式回溯陷阱(代码片段)

...ff09;正则匹配原理》《一起学习正则表达式(七)回溯陷阱》0.写在前面在上一篇文章中,我们学习了正则表达式的匹配原理,在我们常用的开发语言中,大多数都是采用的传统型NFA引擎,也就是非确定性... 查看详情

一起学习正则表达式回溯陷阱(代码片段)

...ff09;正则匹配原理》《一起学习正则表达式(七)回溯陷阱》0.写在前面在上一篇文章中,我们学习了正则表达式的匹配原理,在我们常用的开发语言中,大多数都是采用的传统型NFA引擎,也就是非确定性... 查看详情

学习数据结构笔记=====>栈(代码片段)

...ff08;Java数据结构与算法)案例引入;一个计算器的运算原理比如说输入5*6+3-2;计算器收到的是一个字符串,他就得一个一个分隔这些字符;然后计算;栈(Stack)先进后出的有序列表结构;(FILO)最先放入的元素在栈底,最后放入的在栈... 查看详情

用封装的栈回溯类捕获段错误(代码片段)

本文介绍使用自封装的backtrace类对段错误进行捕获,以方便分析运行错误的方法。并给出实现和测试代码。背景我们写程序难免会运行出错,常在河边,哪能不湿鞋。出错不可怕,怕的是无法定位问题,像段... 查看详情