当前位置: 首页 > news >正文

用户栈的高效解析逻辑

一、背景

在之前的博客 内核逻辑里抓取用户栈的几种方法-CSDN博客 里,介绍了使用内核逻辑进行用户栈的函数地址的抓取逻辑,但是并没有涉及如何解析出函数符号的逻辑。

就如perf工具一样,它也是分为两个步骤,一个步骤是内核态抓取函数地址的PC调用链,第二个步骤则是在用户态逻辑里根据抓取的PC转换成人可阅读的函数符号名字。

当前如果不去考虑解析的性能的话,那么只要拿到elf的路径及offset(具体参考之前的博客 内核逻辑里抓取用户栈的几种方法-CSDN博客),然后通过addr2line就可以解析出用户栈的调用链符号,但是这样解析的话,重复的进行进程创建及退出,性能是不够好的。这篇博客里,我们就讲如何高效地进行用户栈的调用链符号的解析。

二、高性能解析的核心思想

2.1 复用elf的解析结果,预解析常用的so

该高性能解析的核心思想就是启动一个用来解析elf和进程的maps表的应用服务,该应用服务可以以守护进程方式长期运行,被解析请求触发来执行。

由于该应用服务是常驻的,所以就避免了反复启动进程重复解析一样的elf文件导致性能损失。该应用服务会把已经解析elf文件得到的信息进行记录,下一次需要解析相同的elf文件时复用之前解析的结果,同样的,下一次需要解析相同的进程时,也可以复用之前的/proc/<pid>/maps的解析结果。

对于elf文件,一般来说,它是不变的。但是要注意,对于/proc/<pid>/maps里的内容,它是可能变化的,如程序使用dlopen/dlclose这种动态加载和解加载so库的函数,就会触发maps的变更,在实现时需要考虑这样的情况。

另外,为了进一步提升性能,我们可以在该进程启动后,先把一些常用的so库先解析出来,这样在真正接到要解析的请求时由于之前已经解析过了,就可以减少第一次解析某个so的elf文件的耗时。

2.2 使用map容器的upper_bound函数快速找到对应的符号及偏移

假设我们已经解析了某个elf的文件,拿到了函数表,我们如何能快速找到对应的符号呢?

我们可以使用map容器的upper_bound接口来实现这样的快速查找,并且map容器的下标得用地址区间的end,而不能用地址区间的start。

这算是一个小算法,但是也有一定的细节,不能用lower_bound,也不能用start作为key,否则会出现解析不符合预期及解析错误的情况。

这里面主要考虑的就是地址区间通常来说是指[start, end)这么一个区间,也就是start是大于等于,而end是小于。

三、完整的解析步骤

这里说的完整的解析步骤是假设了已经拿到了用户栈调用链的PC的情况下的。至于如何抓取用户栈PC,在之前的博客里 内核逻辑里抓取用户栈的几种方法-CSDN博客 给出了通过内核态逻辑抓取的方法。

我们分析,如果根据进程的pid及进程的PC地址的va,得到对应的函数符号和offset。

3.1 先根据进程的pid获取到进程的elf和maps信息的管理对象

每个进程都对应有一个管理对象,来管理进程的相关与解析函数符号逻辑有关的信息,最主要就是/proc/<pid>/maps,其他信息则是用于辅助输出的内容,比如cmdline内容,这些辅助内容的输出可以帮助定位是具体哪个进程,因为有时候进程名是一样的(/proc/<pid>/comm),但是cmdline是不一样的,可以看出一些细节信息。

关于/proc/<pid>/maps的解析,参考如下逻辑:

if (unlikely(!READ_PROC_MAPS(i_processid, [&](char *i_obuf, int i_obufsize) -> bool {unsigned long start;unsigned long end;char permissions[5]; // 读、写、执行、共享unsigned long offset;char pathname[HARDLINK_MAXBYTE];int ret = sscanf(i_obuf, "%lx-%lx %4s %lx %*x:%*x %*u %s",&start, &end, permissions, &offset,pathname);if (ret == 5 && permissions[2] == 'x') {...
#if (DEBUG_LOG == 1)printf("range: %lx-%lx, permission: %s, offset: %lx, hlink: %s\n",start, end, permissions, offset, pathname);
#endifreturn true;}else {return false;}}))) {...break;}

上面的逻辑里使用了lambda表达式,可以简化逻辑。

3.2 根据elf路径进行增量解析

所谓“增量”解析,也就是指已经解析过的elf文件不再重复解析,因为我们已经保存下来之前解析出来的结果了。

我们可以用一个map来保存已经解析过的内容,key表示elf绝对路径。

如果之前没有解析过相关的elf,则使用objdump -t来进行解析,要注意,务必使用objdump -t来解析,因为objdump -t可以解析出弱符号和static的局部符号,而objdump -T则解析不出这些符号。

objdump的命令如下:

"objdump -t %s | grep -E '\\.text'"

上面的%s替换成elf的绝对路径。

3.3 解析vdso及vsyscall的符号

不管哪个平台,一般都有vdso的符号,但是vsyscall则不同的平台不一样,x86上是有的。

有关vdso和vsyscall的基础介绍和相关内核逻辑和glibc逻辑的相关细节见之前的博客 vdso概念及原理,vdso_fault缺页异常,vdso符号的获取-CSDN博客 和 vdso内核与glibc配合的相关逻辑分析-CSDN博客。

3.3.1 vdso符号表的获取

vdso的符号表的获取,我们是通过dd命令从系统上一般都存在的systemd进程里捞取取出相关的so文件内容,并通过objdump进行解析。

通过dd命令捞取vdso.so文件的命令如下:

if (unlikely(!PROC1MAPS_GREP_VDSO([&](char *i_obuf, int i_obufsize) -> bool {unsigned long vdso_va_begin;unsigned long vdso_va_end;if (sscanf(i_obuf, "%lx%*c%lx", &vdso_va_begin, &vdso_va_end) == 2) {...
#if (DEBUG_LOG == 1)printf("vdso_va_begin:0x%lx, vdso_va_end:0x%lx, size:0x%lx\n", vdso_va_begin, vdso_va_end, vdso_va_end - vdso_va_begin);
#endifchar systemcmd[256];snprintf(systemcmd, 256, "dd if=/proc/1/mem of=" TEMP_VDSO_SO_FILE " skip=%lu ibs=1 count=%lu\n", vdso_va_begin, vdso_va_end - vdso_va_begin);system(systemcmd);return true;}return false;}))) {// 出错了return psyminfo;}

上面PROC1MAPS_GREP_VDSO则是执行如下的命令:

"cat /proc/1/maps | grep -E '\\[vdso\\]'"

然后通过sscanf解析出vdso.so的va的begin和end,然后通过dd命令去dump,dump到一个临时文件中。

然后再通过如下的objdump命令进行解析:

"objdump -T %s | grep -E '\\.text'"

上面的%s则是vdso.so的临时文件的路径。

3.3.2 vsyscall符号表的获取

vsyscall的符号表则是直接根据对应平台的内核里的相关符号的内容情况,手动进行组装。有关vsyscall的符号信息如何查看,参考之前的博客 vdso概念及原理,vdso_fault缺页异常,vdso符号的获取-CSDN博客 里的 4.2 一节。

下面的是大致的拼凑逻辑:

..* ...() {..* psyminfo;...// vsyscall目前只用考虑x86场景,x86的vsyscall的情况是固定的,就三个符号...unsigned long start = 0;unsigned long span = 1024;psysrange = ...psysrange->start = start;psysrange->span = span;strscpy(psysrange->sym, "gettimeofday", SYM_MAXBYTE);
#if (DEBUG_LOG == 1)printf("start:0x%llx, span:0x%llx, typestr:%s, symname:%s \n",psysrange->start, psysrange->span, HLINK_VSYSCALL, psysrange->sym);
#endifstart += 1024;...psysrange->start = start;psysrange->span = span;strscpy(psysrange->sym, "time", SYM_MAXBYTE);
#if (DEBUG_LOG == 1)printf("start:0x%llx, span:0x%llx, typestr:%s, symname:%s \n",psysrange->start, psysrange->span, HLINK_VSYSCALL, psysrange->sym);
#endifstart += 1024;...psysrange->start = start;psysrange->span = span;strscpy(psysrange->sym, "getcpu", SYM_MAXBYTE);
#if (DEBUG_LOG == 1)printf("start:0x%llx, span:0x%llx, typestr:%s, symname:%s \n",psysrange->start, psysrange->span, HLINK_VSYSCALL, psysrange->sym);
#endifreturn psyminfo;}

3.4 通过upper_bound来查找对应的符号及offset

有关为什么用upper_bound在上面 2.2 里做了简要说明。

大致的代码如下:

..* ...(u64 i_addr) {auto it = xx.upper_bound(i_addr);//printf("count[%d]\n", xx.size());if (it != xx.end()) {if (it->second->start <= i_addr && i_addr < it->second->start + it->second->span) {return it->second;}}return NULL;}

3.5 成果展示

我们使用perf来抓取进程pid是1的systemd这个进程的用户态符号,然后,再用该程序进行解析,看解析出的内容是否一致。

用perf record -g -p 1之后,用perf script得到的如下图的一处采样:

我们使用解析程序,输入pid和va,进行解析,可以看到解析出一样的函数符号名和offset。

相关文章:

  • EtherNet/IP机柜内解决方案在医疗控制中心智能化的应用潜能和方向分析
  • springMVC拦截器,拦截器拦截策略设置
  • 【动手学深度学习】系列
  • ShenNiusModularity项目源码学习(27:ShenNius.Admin.Mvc项目分析-12)
  • ABC 355
  • DeepSeek的走红,会不会带动芯片市场新一轮增长?
  • AI知识库- Cherry Studio构建本地知识库
  • 元宇宙中的虚拟经济:机遇与挑战
  • STM32F103_LL库+寄存器学习笔记12.2 - 串口DMA高效收发实战2:进一步提高串口接收的效率
  • C++ 空间配置器
  • 【周输入】517周阅读推荐-1
  • 数组的概述
  • 大模型(3)——RAG(Retrieval-Augmented Generation,检索增强生成)
  • JAVA基础——数组与二维数组
  • 基于Python批量删除文件和批量增加文件
  • Linux 下 rsync 工具详解与实用指南
  • 数据库 1.0.1
  • 如何使用通义灵码提高前端开发效率
  • FastDatasets新功能,让模型学会“思考”!
  • 文件操作和IO-2 使用Java操作文件
  • 台湾做甜品的网站/企业网络营销策划方案
  • 做vb程序的网站/2022最新版百度
  • 网站被主流搜索引擎收录的网页数量是多少/网店运营
  • 做健康食品的网站/广州网站优化公司
  • 购物网站建设要多少钱/电商网站建设 网站定制开发
  • wordpress 网站搬家/互联网营销师培训费用是多少