关于人生,有一句著名的话,说人生就是一场修行。又说,修行就是走一条路,一条很长的路。

如果套用这个说法,我选择的一条修行之路就是软件调试,进一步说就是穷究软件调试的方方面面,什么是软件调试,怎么用软件调试的方法和理论来调试软件。软件有很多种,针对不同的软件,“软件调试”和“调试软件”又有哪些相同和不同。

时代在变化,软件的形势也在变化,研究了很多年的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编译出来的文件啊。

于是,更切实的方法还是修改自己的代码,在栈回溯时接受没有函数身份的符号。说改就改,编译执行,问题好了,一个完美的栈回溯终于呈现在了面前。

用了几个小时的时间,一路搜索、调试、分析、追踪,终于把悬挂很久的一个老大难问题解决了,站起身,窗外阳光灿烂,天高云淡,秋色盎然。

人生是一场修炼,在这场修炼中,不要觉得目标很遥远,千里之行始于一步,每前进一步,就距离目标更近一些。

***********************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

欢迎关注格友公众号

为求一层栈,追踪八万里相关推荐

  1. PAT甲级1115 Counting Nodes in a BST (30分):[C++题解] 递归建二叉搜索树、dfs求一层结点数量

    文章目录 题目分析 题目链接 题目分析 分析 首先本题给定的二叉搜索树的定义和其他地方的不同.本题小于等于的是左子树,右子树是大于根结点的. 然后说一下做题的思路. 给定一串数据,让构造二叉搜索树. ...

  2. 用deque模拟栈解决八皇后问题

    因为栈stack的遍历只能逐个pop,很麻烦,并且破坏了原来的栈,所以可以用deque来模拟栈. #include "stdafx.h" #include <iostream ...

  3. 2015 UESTC 数据结构专题N题 秋实大哥搞算数 表达式求值/栈

    秋实大哥搞算数 Time Limit: 1 Sec  Memory Limit: 256 MB 题目连接 http://acm.uestc.edu.cn/#/problem/show/1074 Des ...

  4. python小白到全栈_python全栈第八讲

    一.什么是函数 当我们在写代码时需要多次使用某段代码时,为避免代码重复,可将该段代码封装起来,哪里需要就在哪里调用这段代码,这段封装起来的代码就是函数. 函数是组织好的,可重复使用的,用来实现单一,或 ...

  5. Python全栈(八)Flask项目实战之8.CMS后台轮播图管理

    文章目录 一.首页轮播图 二.CMS轮播图管理页面 三.CMS添加轮播图 四.CMS轮播图编辑和删除 五.七牛云介绍 人生是需要奋斗的,只有你奋斗了,失败后才会问心无愧: 人生是单行路,只有奋斗了,才 ...

  6. Python全栈(八)Flask项目实战之6.前台注册功能开发

    文章目录 一.前台用户模型的定义 二.注册页面完成和图形验证码类 1.注册和登录页面搭建 2.图形验证码的实现 三.点击更换图形验证码 四.发送短信验证码 五.短信验证码接口加密验证和JS代码加密 如 ...

  7. Python全栈(八)Flask项目实战之2.CMS后台功能开发

    文章目录 一.CMS用户登录功能 二.CMS用户错误信息返回 三.CMS用户登录限制和CSRF保护 1.登录验证 2.CSRF保护 四.CMS用户名渲染和注销功能 1.后台页面基本实现 2.用户名渲染 ...

  8. Python全栈(八)Flask项目实战之10.前台发布帖子和后台帖子管理页面搭建

    文章目录 一.前台板块页面搭建 二.发布帖子页面搭建 三.前台帖子模型创建 四.文章的发布 1.基本实现 2.项目优化 (1)功能优化--Markdown编辑上传上传本地图片 (2)代码优化--抽离A ...

  9. c语言求不定式的最大值,C语言之四则运算表达式求值(链栈)—支持浮点型数据,负数, 整型数据运算...

    运算符间的优先级关系: 链栈结构体定义: 数据域使用字符串长度为20的字符数组(故需要注意判断读取的字符串是运算符还是数值) 可支持浮点型数据,负数, 整型数据的运算 float EvaluateEx ...

  10. 【数据结构】用栈解决表达式求值问题

    题目:求4+4/2-9*3的值: 思路: ①:用一个字符型数组存放了表达式<4+4/2-9*3>: 1 char val[9] = {'4','+','4','/','2','-','9' ...

最新文章

  1. Redis 的性能幻想与残酷现实(转)
  2. HDU-4516 威威猫系列故事——因式分解 多项式分解
  3. 地壳中元素含量排名记忆口诀_【中考化学】初中化学记忆性知识点03-生活中的化学-生活常识...
  4. docker 添加端口映射_Docker快速搭建PHP开发环境详细教程
  5. java wait源码_Java精通并发-透过openjdk源码分析wait与notify方法的本地实现
  6. MUSICAL CHAIRS【模拟】
  7. niginx反向代理解决前后端跨域问题
  8. c#面向对象与程序设计第三版第三章例题代码_C#程序设计教程 | 教与学(教学大纲)...
  9. 董明珠今晚开启抖音直播首秀;传苹果将去掉 iPhone 闪电接口;PyTorch 1.5 发布 | 极客头条...
  10. java 正则表达式案例
  11. Kubernetes 小白学习笔记(32)--kubernetes云原生应用开发-sidecar注入和istio服务治理演示
  12. requests模块中使用代理proxy发送请求
  13. Can‘t commit changes due to unresolved conflicts
  14. FoxNFT创世品牌娘卡包预售6月15日正式开启!五位姑娘正式与大家见面
  15. 高速电路中菊花链、fly-by与T点拓扑
  16. 华三和华为交换机配置FTP文件传输
  17. asp毕业设计——基于asp+access的网上投票系统设计与实现(毕业论文+程序源码)——网上投票系统
  18. android设置应用字体大小,在Android应用程序改变的TextView的字体大小从原始设置更改字体大小(Font size...
  19. python-画3D图
  20. 【Java】Java实现找图抓色

热门文章

  1. 电脑证书错误即上网站打不开提示证书错误
  2. 医院网络广告的结算形式-医院网络营销站外合作篇
  3. 人体动作捕捉与SMPL模型 (mocap and SMPL model)
  4. HttpWatch软件介绍与基本使用
  5. 论坛勋章动态特效制作流程
  6. mysql 错误代码1130_mysql出现错误码1130怎么办
  7. 以下哪些属于计算机应用领域,以下哪些计算机的应用领域?()
  8. 用javascript的正则表达式来验证Email地址是否格式正确
  9. 【Sass/SCSS】预加载器中的“轩辕剑”
  10. 永洪科技何春涛:中国企业数据技术的6大需求和解决之道