为求一层栈,追踪八万里
关于人生,有一句著名的话,说人生就是一场修行。又说,修行就是走一条路,一条很长的路。
如果套用这个说法,我选择的一条修行之路就是软件调试,进一步说就是穷究软件调试的方方面面,什么是软件调试,怎么用软件调试的方法和理论来调试软件。软件有很多种,针对不同的软件,“软件调试”和“调试软件”又有哪些相同和不同。
时代在变化,软件的形势也在变化,研究了很多年的Windows平台调试之后,这些年也花了大把大把的时间在Linux上。
众所周知,Linux平台上的调试工具积贫积弱,没法与Windows平台相比。以内核调试为例,KDB、KGDB等方式都有很多不足。
对于这样的情况,我们没有权力抱怨,因为在自由软件的世界里,这是很正常的事。
写到这里,不由得想起缔造GNU世界的Richard Stallman先生,也是GDB调试器的作者,在他亲自撰写的GDB教程封面上引用的一句话:“如果它不工作,那么别焦虑。如果所有东西都工作了,那么你就没有工作了。”
是啊,在自由软件的世界里,已经有的都是别人奉献出来的,还缺少的,你如果不满意,那么你可以自己动手做啊。
因为很多原因,我花了很多时间在一个新的调试器上,我精心规划它的每个功能,精心设计它的每段代码,有些代码写了一遍之后,再写一遍。
我把这个新的调试器,取名叫Nano Debugger,简称NDB。
开发一个新的调试器需要大量的时间。自从把《软件调试》第2卷交稿后,我不仅把我的大多数业余时间都用在NDB上,而且还花了不少的正常工作时间,年轻的格蠹科技也投了不小的人力物力在这个工具上。
天道酬勤,每一份付出都有收获,在我和格蠹小伙伴们的努力下,NDB一天天成长。
加入一个个新的功能,去除一个个瑕疵。
如果把NDB当作一个孩子,那么我给它设定的目标有两个:一是有深度,具有最底层的控制能力,能观察到系统的最底层信息;二是有高度,能呈现软件世界的高层语义,让调试者可以快速理解问题现场。
对于这两个目标,说来容易,实现起来,真是困难重重。
要实现底层控制,那么必须要有硬件支撑,为此,格蠹科技专门打造了一个调试套件,取名为GDK7。
GDK是Gedu Debug Kit的缩写,那么为什么叫GDK7呢?因为GDK7的CPU是Intel 的。在Intel的CPU设计团队里,有个以调试技术见长的印度人,它的名字以K开头,字母很多,不好读,因为字母的个数是7个,于是很多人就叫他K7。
再说第二个目标,要想呈现高层语义,就必须依赖调试符号。可是调试符号种类繁多,格式多变,受源代码、编译器等多种因素影响,要想搞定这个拦路虎也非一日之功。
下面举个具体的例子。栈是CPU的贴身行囊,里面记录着CPU的行动轨迹。把这个轨迹呈现出来就可以看到CPU从那里来,即将往那里去。这个功能一般称为栈回溯,是调试器的最重要功能之一。在GDB中,bt(backtrace)命令用于此,在WinDBG中,k命令用于此。
在NDB中,既可以用k命令,也可以用bt命令来激发栈回溯。比如下图便是把Linux内核中断下来后,经常看到的一个栈回溯结果,显示的是著名的idle线程的执行经过。
仔细观察上面的栈回溯,它非常详细的记录了从内核初始化到功成名就,进入idle循环休息的旅程。
lk!intel_idle+0x87
lk!cpuidle_enter_state+0x75
lk!cpuidle_enter+0x2e
lk!call_cpuidle+0x23
lk!do_idle+0x1f6
lk!cpu_startup_entry+0x1d
lk!rest_init+0xae
lk!arch_call_rest_init+0xe
lk!start_kernel+0x56c
lk!x86_64_start_reservations+0x24
lk!x86_64_start_kernel+0x74
lk+0x10d4
虽然上述结果已经比较完整,足以让很多人感觉惊奇。但是对于我来说,它还不够完美,有个不足,那就是最底下的那个栈帧没有显示出函数名称,只能用模块基地址+偏移的方法显示为lk+0x10d4,如果显示出来,它应该是x86_64_start_kernel函数的父函数。
我发现这个不足很久了,知道它是个不足,是个有待完善的地方,但是一直没有时间去深究它。
最近的天气非常好,大多都是晴天,阳光充足,气温舒适。很多树木的叶子都变成美丽的彩色,可谓层林尽染,秋高气爽。这样的好天气,真的想在周末出去走走,爬爬山。但是为了NDB,我还是决定坐下来debug,只能在房间里享受秋日的美好阳光。
周六的早晨,吃过早饭,我坐下来,准备打一场歼灭战,为x86_64_start_kernel寻找父函数,找不到不罢休,“不解决不收兵吃饭”(^-^)。
何处下手呢?
搜源代码。
Linux内核是开源的,有很多便利之处,一定要充分利用。
缺少的函数是x86_64_start_kernel的父函数,因此先搜一下x86_64_start_kernel。
今天的Linux内核源代码总量可谓“浩然”,压缩包110MB,解压后接近1GB大关。
存放Linux源代码的机械硬盘“哗哗的”响了足足几分钟后,搜索结果逐渐展现在我面前。
先说最后两个结果,都是以某种方式产生的栈回溯结果,但都是把x86_64_start_kernel认定为是最后一个栈帧,直接没有显示任何关于父函数的信息,还不如NDB。
第1和第2个结果分别是原型声明和实现。
asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
特别说明一下这个原型中的asmlinkage,它意味着,这个函数是可以被汇编代码所调用的,也就是它是C函数与汇编函数的交界。
第二个结果是在head.S中,发现了x86_64_start_kernel,这显然是最有价值的信息。因为head_64.S中.S代表它里面是汇编代码,head之名是Linux内核惯用的代表“内核之头”的意思。
展开第2个搜索结果,内容如下:
看这部分代码,显然不是汇编指令,而是写给汇编器的指示符(directive)。
线索断了么?
没有,在上图.quad指示符上面为这个变量定义了一个符号initial_code,在这个文件里搜一下initial_code,就柳暗花明又一村了。
这下看到的是汇编指令了,著名的AT&T格式,比较难度,但是习惯了也还好。
因为这一段代码特别重要,所以特别再以文字形式摘录如下:
pushq $.Lafter_lret # put return address on stack for unwinder
xorl %ebp, %ebp # clear frame pointer
movq initial_code(%rip), %rax
pushq $__KERNEL_CS # set correct cs
pushq %rax # target address in negative space
lretq
.Lafter_lret:
END(secondary_startup_64)
上面几句汇编指令的含义大致为:
把标号.Lafter_lret的地址压入栈,后面的注释还特别说,这是给栈展开使用的,栈展开是栈回溯的另一种说法。
把ebp寄存器清零
把initial_code指针的值赋给rax
把__KERNEL_CS的值压栈
把rax寄存器的值压栈
执行函数返回指令
最有趣的是最后一条指令,字面上是从函数返回的ret指令,其实这里是用来调用函数。因为ret指令的操作是从栈上弹出一个值,赋给程序指针,所以它也是可以用来调用函数的,很多黑客喜欢用这样的写法,通常称为“倒车”。
如此看来,上述这个代码片段就是调用x86_64_start_kernel的地方。它叫什么名字呢?根据END语句中的信息,它叫secondary_startup_64。向上翻一下,也可以看到这个汇编函数的入口:
ENTRY(secondary_startup_64)
经过上面一番搜索和分析,算是找到了x86_64_start_kernel的父函数名称,叫secondary_startup_64,接下来的问题是为什么NDB没有找到这个函数呢?
尝试在NDB中使用x命令来显示所有以startup_64结束的符号,结果让人惊喜,明明有啊。
x lk!*startup_64
ffffffff`aa0001f0 lk!__startup_64 (int64, boot_params*)
ffffffff`aa000000 lk!startup_64
ffffffff`aa000030 lk!secondary_startup_64
是位置信息对不上么?
因为Linux内核有地址随机化功能,每次启动时内核函数都可能改变地址。但是以固定的一次启动来计算:
x86_64_start_kernel的返回地址为ffffffff`aa0000d4
lk!secondary_startup_64的起始地址为ffffffff`aa000030
在调试器下找到secondary_startup_64符号,观察它的size信息,是0xa4
把ffffffff`aa000030+0xa4,刚好等于 ffffffff`aa0000d4,这意味着栈回溯中找到的返回地址刚好是secondary_startup_64的末尾,这与汇编代码中标号在汇编函数的最后也是一致的。
那么是什么原因让NDB没能显示出secondary_startup_64呢?
一边跟踪代码,一边思考,想到的另一个原因可能是符号类型的问题,对于这个汇编语言的函数,GCC没有为其产生DWARF格式的符号,只产生了简单的ELF格式符号,ELF符号也有如下几种类型:
//ELF SymType.
#define SYMTYPE_NOTYPE 0
#define SYMTYPE_OBJECT 1
#define SYMTYPE_FUNC 2
#define SYMTYPE_SECTION 3
#define SYMTYPE_FILE 4
#define SYMTYPE_LOPROC 13
#define SYMTYPE_HIPROC 15
上面截图中的m_info字段的低四位用来记录SymType,仔细观察上面截图中的m_info值,它的低四位都是0,代表SYMTYPE_NOTYPE,没有类型。
在NDB的代码中,栈回溯时只寻找函数类型的符号,因此找不到secondary_startup_64这个符号。
问题的根源终于找到了,是GCC的问题么?为什么不给汇编函数一个函数身份?汇编写的函数难道就不是函数么?谁写的代码,我真想找他问个明白。
可是即使GCC的同行承认这是个bug,也不可能立刻修正啊,即使修正了,很多用户也不会立刻更新到没有问题的GCC啊,即使更新了,还有很多老版本GCC编译出来的文件啊。
于是,更切实的方法还是修改自己的代码,在栈回溯时接受没有函数身份的符号。说改就改,编译执行,问题好了,一个完美的栈回溯终于呈现在了面前。
用了几个小时的时间,一路搜索、调试、分析、追踪,终于把悬挂很久的一个老大难问题解决了,站起身,窗外阳光灿烂,天高云淡,秋色盎然。
人生是一场修炼,在这场修炼中,不要觉得目标很遥远,千里之行始于一步,每前进一步,就距离目标更近一些。
***********************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
欢迎关注格友公众号
为求一层栈,追踪八万里相关推荐
- PAT甲级1115 Counting Nodes in a BST (30分):[C++题解] 递归建二叉搜索树、dfs求一层结点数量
文章目录 题目分析 题目链接 题目分析 分析 首先本题给定的二叉搜索树的定义和其他地方的不同.本题小于等于的是左子树,右子树是大于根结点的. 然后说一下做题的思路. 给定一串数据,让构造二叉搜索树. ...
- 用deque模拟栈解决八皇后问题
因为栈stack的遍历只能逐个pop,很麻烦,并且破坏了原来的栈,所以可以用deque来模拟栈. #include "stdafx.h" #include <iostream ...
- 2015 UESTC 数据结构专题N题 秋实大哥搞算数 表达式求值/栈
秋实大哥搞算数 Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://acm.uestc.edu.cn/#/problem/show/1074 Des ...
- python小白到全栈_python全栈第八讲
一.什么是函数 当我们在写代码时需要多次使用某段代码时,为避免代码重复,可将该段代码封装起来,哪里需要就在哪里调用这段代码,这段封装起来的代码就是函数. 函数是组织好的,可重复使用的,用来实现单一,或 ...
- Python全栈(八)Flask项目实战之8.CMS后台轮播图管理
文章目录 一.首页轮播图 二.CMS轮播图管理页面 三.CMS添加轮播图 四.CMS轮播图编辑和删除 五.七牛云介绍 人生是需要奋斗的,只有你奋斗了,失败后才会问心无愧: 人生是单行路,只有奋斗了,才 ...
- Python全栈(八)Flask项目实战之6.前台注册功能开发
文章目录 一.前台用户模型的定义 二.注册页面完成和图形验证码类 1.注册和登录页面搭建 2.图形验证码的实现 三.点击更换图形验证码 四.发送短信验证码 五.短信验证码接口加密验证和JS代码加密 如 ...
- Python全栈(八)Flask项目实战之2.CMS后台功能开发
文章目录 一.CMS用户登录功能 二.CMS用户错误信息返回 三.CMS用户登录限制和CSRF保护 1.登录验证 2.CSRF保护 四.CMS用户名渲染和注销功能 1.后台页面基本实现 2.用户名渲染 ...
- Python全栈(八)Flask项目实战之10.前台发布帖子和后台帖子管理页面搭建
文章目录 一.前台板块页面搭建 二.发布帖子页面搭建 三.前台帖子模型创建 四.文章的发布 1.基本实现 2.项目优化 (1)功能优化--Markdown编辑上传上传本地图片 (2)代码优化--抽离A ...
- c语言求不定式的最大值,C语言之四则运算表达式求值(链栈)—支持浮点型数据,负数, 整型数据运算...
运算符间的优先级关系: 链栈结构体定义: 数据域使用字符串长度为20的字符数组(故需要注意判断读取的字符串是运算符还是数值) 可支持浮点型数据,负数, 整型数据的运算 float EvaluateEx ...
- 【数据结构】用栈解决表达式求值问题
题目:求4+4/2-9*3的值: 思路: ①:用一个字符型数组存放了表达式<4+4/2-9*3>: 1 char val[9] = {'4','+','4','/','2','-','9' ...
最新文章
- Redis 的性能幻想与残酷现实(转)
- HDU-4516 威威猫系列故事——因式分解 多项式分解
- 地壳中元素含量排名记忆口诀_【中考化学】初中化学记忆性知识点03-生活中的化学-生活常识...
- docker 添加端口映射_Docker快速搭建PHP开发环境详细教程
- java wait源码_Java精通并发-透过openjdk源码分析wait与notify方法的本地实现
- MUSICAL CHAIRS【模拟】
- niginx反向代理解决前后端跨域问题
- c#面向对象与程序设计第三版第三章例题代码_C#程序设计教程 | 教与学(教学大纲)...
- 董明珠今晚开启抖音直播首秀;传苹果将去掉 iPhone 闪电接口;PyTorch 1.5 发布 | 极客头条...
- java 正则表达式案例
- Kubernetes 小白学习笔记(32)--kubernetes云原生应用开发-sidecar注入和istio服务治理演示
- requests模块中使用代理proxy发送请求
- Can‘t commit changes due to unresolved conflicts
- FoxNFT创世品牌娘卡包预售6月15日正式开启!五位姑娘正式与大家见面
- 高速电路中菊花链、fly-by与T点拓扑
- 华三和华为交换机配置FTP文件传输
- asp毕业设计——基于asp+access的网上投票系统设计与实现(毕业论文+程序源码)——网上投票系统
- android设置应用字体大小,在Android应用程序改变的TextView的字体大小从原始设置更改字体大小(Font size...
- python-画3D图
- 【Java】Java实现找图抓色
热门文章
- 电脑证书错误即上网站打不开提示证书错误
- 医院网络广告的结算形式-医院网络营销站外合作篇
- 人体动作捕捉与SMPL模型 (mocap and SMPL model)
- HttpWatch软件介绍与基本使用
- 论坛勋章动态特效制作流程
- mysql 错误代码1130_mysql出现错误码1130怎么办
- 以下哪些属于计算机应用领域,以下哪些计算机的应用领域?()
- 用javascript的正则表达式来验证Email地址是否格式正确
- 【Sass/SCSS】预加载器中的“轩辕剑”
- 永洪科技何春涛:中国企业数据技术的6大需求和解决之道