嵌入式软件调试
2025-06-01 19:50:16
嵌入式软件调试理论基础
• 什么是软件调试?
• 英文software debug,又译软件侦错
• 软件调试过程,就是发现软件失效,定位软件错误并将其修复的过程
软件调试的重要性
• 据统计:软件调试、 debug时间一般占软件开发周期的50%以上,是软件开发中耗时最多的一项活动
• 很多项目延期,往往就栽在不能定位的bug上。
• 随着软件、系统越来越复杂,软件调试技术需要与软件工程、开发技术同步升级
• 软件调试理论和知识尚未系统化,很多开发者对其基本原理知之甚少,不能根据实际情况融会贯通地去使用各种调试技巧,对于复杂问题、BUG往往一筹莫展、无能为力
软件调试的特点
• 是一项具有挑战性、很强技巧性的工作
• 复杂度高、难度大,必须通过现象,大量的分析,才能逐步接
近真相,犹如福尔摩斯探案,抓住蛛丝马迹,层层推理。
• 需要知识面广:设计到硬件、软件、操作系统、编译器等。
• 有些bug极难复现,定位困难。
• 是一项不受欢迎的工作
• 对心理影响:
– 打击人的自信、消磨工作热情、考验人的耐心和抗压能力、
怀疑人生、怀疑自己是不是这块料…
• 对生理影响:
– 脾气暴躁、上火、口腔溃疡、失眠…
嵌入式软件调试特点
• 调试环境和运行环境不在一个平台上,增
加了调试的难度
• 嵌入式设备往往没有输出、打印终端,遇
到问题,只能看日志
• 考虑问题还要考虑硬件问题、时序问题,
需要软硬件结合去思考问题,增加了调试
的难度
软件调试一些参考经验
• 寻找类似的bug,一些常见的出错场景
• 使用版本管理工具查看最近代码的变化
• 打印有时候会失效:比如在打印之前程序已经crash
• 尽可能深入准确理解系统、不适当的改动可能使事情更糟
• 体力不支时、没思路时可以稍事休息,保持充沛精力
• 对于多模块系统,多沟通、多交流,而不是相互推诿
常用的嵌入式软件调试工具
• 软件
• IAR、 ADS/AXD、 Keil、 MDK、 RVDS、 Eclipse、 H-JTAG、 Trace32
• GCC、 GDB、 KGDB、 JDB、
• 性能分析工具、内存分析工具
• 硬件
• 万用表、示波器、逻辑分析仪、仿真器
• Jlink、 JTAG
• 常用的软件调试技术
软件调试技术分类
• 按目标代码的执行方式
• 脚本调试、托管调试、本地调试、混合调试
• 按目标代码的执行模式
• 用户态调试、内核态调试
• 按软件所处的阶段
• 开发期调试、产品期调试
• 按调试器和调试目标的相对位置
• 本地调试、远程调试
打印
• 输出调试信息
• 打印函数栈、变量值
• 日志、文件转储
• 应用场合
• 错误简单,直接打印比使用调试器方便
• 难以使用调试器的环境、或者使用调试器无法重现
调试器
• 断点
• 单步执行
• 事件追踪
• 栈回溯
• 反汇编
• 观察和修改寄存器、内存数据
• 控制被调试的进程或线程
使用调试器有哪些优点
• 不需要预知错误在哪里
• 支持在线检查错误,不需要改代码、重新编译
• 可以看到运行时的各种数据:变量值、寄存器、内存数据…
• 单步
调试器调试一般步骤
• 定位出现错误的场景
• 分析错误、粗略定位可能出错的代码
• 设置初始断点
• 开始调试程序或者attach一个已经运行的
程序进程
• 在断点上观察数据各种数据:变量、寄存
器、调用栈、反汇编, dump有用的数据
• 单步执行、更新断点
• 结束调试器
• 常见的错误类型
编译型错误
• 语法规则检查
• C语言的基本语法、关键字、运算符、表达式
• 中英文符号
• 全角、半角的问题
• 函数问题
• 函数声明与函数定义不匹配:函数参数、返回类型
• 误认为形参改变会影响实参的值
• 函数的实参和形参类型不一致
• 函数未声明
• 有时候一个warning也是引起软件失效的诱因
运行时错误
• 对异常未做处理
• 打开的文件未找到
• 磁盘空间不足
• 内存不足
• 网络异常
• Scanf输入格式、忘记地址符
• 堆栈溢出
• 空指针的引用
• 未初始化局部变量
• 数组问题
• 数组越界, C语言并不会对数组做边界检查
• 数组下标
• 混淆数组名与指针的区别、数组作为参数的无用
逻辑错误
• 运算符
• =和==、 &和&&、 |和||混用
• 运算符的优先级和结合性
• 循环条件设置问题
• 未注意int、 char类型的数值范围,导致死循环
• 循环边界控制
• 链表的头尾判断、空链表处理
• 业务逻辑错误
• 死锁
• 不良的编程习惯和代码风格
• 不该加分号的地方加了分号或者少加分号
• 花括号忘记使用,导致错误的逻辑分支
内存错误
• 内存溢出
• 内存泄露
• 内存踩踏
• Debug文件和release文件的区别
目标文件的链接过程
可执行文件的运行
• 调试符号
什么是调试符号
• 二进制代码与源程序联系的桥梁
• 很多调试必须依赖调试符号才能工作
• 如:源代码级调试、 栈回溯、按名称显示变量等
• 生成过程
• 在编译过程中,编译器从源文件收集调试信息供开发者调试使用
• 这些信息以表格形式记录在符号表中,是对源程序的概括
• 包括变量、类型、函数、标号和源代码行等。
• 存储方式
• 由编译器收集和提炼后,再由链接器或者专门工具保存到调试符
号文件中。
• 调试符号可存储在单独的文件,也可与目标文件共享一个文件
• 调试信息的存储格式
• COFF格式
• 二进制格式,用来存储可执行映像文件、目标文件、库文件
• CodeView格式
• 与MSC编译器一起使用的调试器
• CV格式的调试信息可以与映像文件保存在一起,也可单独存放
• PDB格式
• PDB格式的调试信息需单独存储在一个文件中
• 如VC++6.0中的.pdb文件
• DWARF格式
• 公开的调试信息格式规范,主要用在Unix、 Linux发行系统中
• GCC和GDB都支持这种格式
• 目标文件中的调试信息
image-20250602172333724
• 目标文件
• 编译器用来存放目标代码的文件
• VC使用COFF格式来存储目标文件
• 目标文件格式
• 文件头结构
• 节头部数据结构:
• 三种数据: 原始数据、 重定位信息、 行
号信息
• 节数据之后是调试符号表和字符串表
• 重定位信息和行号信息
• 重定位信息:链接和加载映像文件时应
如何修改节数据、重定位的地址和方法
• 行号信息: 用来描述源代码行和目标代
码的对应关系
• 使用GCC编译debug目标文件
• 断点和单步是怎么实现的?
• CPU对调试的支持
• CPU指令和指令集
• 为某一类CPU所支持的指令集被简称为指令集,根据指令集特征,
CPU可划分为两大阵营: RISC和CISC
• RISC:通过减少指令集数量和简化指令格式来提高和优化CPU执行
指令效率。例如: ARM、 MIPS、 Alpha、 SPARC、 PowerPC
• CISC: X86系列处理器
• CPU对断点调试的支持
• 支持断点调试指令: INT 3指令
• 标志寄存器: EFLAGS寄存器中的TF标志位
• 1: CPU执行完一条指令都会产生调试异常, CPU转到
ISR中去,在该ISR中可以很多调试操作
• 该标志是实现单步调试的基础
• 调试寄存器: DR0~DR7
• JTAG支持:单独靠软件无法调试的裸板、系统bringup调试
• 操作系统对调试的支持
• 在内核层面提供支持
• 提供支持远程调试协议的通信模块
• 提供断点设置函数
• 提供软陷异常处理:调试功能
• 对用户态调试器的支持
• 创建调试目标的系统函数
• 在调试循环中处理调试事件的系统函数
• 查看和修改调试目标的系统函数,这些系统函数用于调试事件
的处理过程中
• 用户态调试器将这些系统函数与其它函数结合起来,从而提供
强大的功能
• 在调试器中加入断点
.
• 单步
• 获取变量值
进程中可以以地址来标识变量、函数,但不知
道每个地址的含义、地址对应的名称
• 调试器使用符号表从地址获得变量名
• 调试信息的内容
• 地址对应的变量名、函数名
• 指令对应的源文件及其行号
• 数据结构的信息等
• 调试信息的保存
• Debug版本程序:添加到目标文件中
• Release版本:单独的符号文件
• DBG文件
• PDB文件
• MAP文件
• 读写寄存器
• CPU对调试的支持
• CPU自身提供的机制
• ARM结构CPU寄存器
• JTAG扫描链电路+ARM 寄存器
• 仿真器调试原理
• 嵌入式常用调试手段
• 软件模拟器
• 在PC上模拟目标CPU并执行用户目标代码。如ARM仿真器: ARM
armulator,可以模拟运行ARM指令系统
• 目标Monitor
• 将目标代码下载到用户目标板的存储器重,并增加一个monitor软
件,用来监听用户目标代码的执行。用户通过串口等调试端口,
通过PC进行调试。如ARM基于调试代理angel的调试
• 缺点:耗费MCU、 CPU资源、目标系统必须是一个完整的系统、
无法在ROM区设置断点、对于存储受限的单片机等并不适用
• 仿真器
• 一般会有一个仿真头、 代替目标系统中的MCU、 CPU, 并仿真其
运行,可以连接目标板,甚至不连接都可以。
• 仿真器运行起来跟实际的目标处理器一样,增加了调试功能、支
持在线调试
• 为什么要使用仿真器调试
• 在前期硬件验证上必须使用仿真器(目标系
统硬件不完整也可以运行)
• 硬件实际性能测试(电路、电容、电感等)
• 额外的优点
• Monitor要占用额外的存储和通信端口,仿真器不需要目标系
统或CPU资源
• 硬件断点:调试ROM或者NOR 存储模式的目标系统毫无压力
• 跟踪功能:能够记录所有的取指操作
• 条件触发
• 实时显示存储器、内存和I/O内容
• 硬件性能分析
Trace32仿真器介绍
• 德国Lauterbach公司研发
• 通用性:更换仿真头子可以调试不同芯片
• 仿真器调试的缺点
仿真器的缺点
• 贵
• 仿真器有自己的目标CPU、 RAM甚至ROM以及软件
• 通用性差
• 随着CPU不断发布、升级,需要同步升级。
• 硬件仿真效果跟实际处理器还是有差异
• 仿真器中的CPU跟目标系统中的CPU电气特性不同
• 不能反映实际时序
• JTAG和JLINK调试原理及区别
• JTAG调试原理
• 测试接口标准化
• 在芯片内部定义一个标准的测试访问接口(TAP ),通过专用的
JTAG测试工具对芯片内部节点进行测试和调试
• JTAG(Joint Test Action Group)
• 一种国际标准测试协议,主要用于芯片内部测试
• 目前大多数芯片都支持JTAG协议: ARM、 DSP、 FPGA等
• JTAG接口
• TMS:测试模式选择
• TCK:测试时钟输入
• TDI: 测试数据输入,数据通过TDI引脚输入JTAG接口
• TDO:测试数据输出,数据通过TDO引脚从JTAG接口输出
• JTAG接口
image-20250602174440435
image-20250602174445412
image-20250602174447903
JTAG实物图
• 边界扫描电路
• 测试访问口TAP
• TAP
• Test Access Port
• 是一个通用端口,通过该端口可以访问数据寄存器和指令寄存
器
• 通过TAP控制器来对整个TAP端口的控制
• 总结
• PC机对目标板的调试就是通过TAP接口完成对数据寄存器DR和
指令寄存器IR的访问。
• Jlink调试原理
• RDI调试接口
• ARM公司提出的调试接口标准,实现跨平台的硬件调试
• 各大调试工具Keil、 ADS、 IAR等都使用RDI公共调试接口
• RDI命令到JTAG协议的转换
– 软件实现
• 在PC写一个程序,将ADS/Keil的RDI命令解析成JTAG协议,然后
通过物理转换接口(仅仅是电气物理层上的转换,像RS232),发
送到目标板。如H-JTAG工具
– 硬件实现
• 做一个电路板,直接接收来自Keil、 ADS等软件的调试命令,硬
件实现RDI到JTAG协议的转换,与目标板通信。如Jlink类似工具
• 相对于软件转换,硬件速度快,而且不需要装jtag解析软件
Jlink工具实物图
printf函数打印高阶技巧
• 输出重定向
• 流的概念
• 程序输入或输出的一个连续的字节序列
• 设备(鼠标、键盘、打印机、屏幕…)的输入输出使用流来处理
• 在C语言中,所有流均以文件的形式出现
• 统一了各种硬件操作接口带来的差异
• C语言中提供的5中标准流
名称 |
---|
stdin |
stdout |
stderr |
stdprn |
stdaux |
• 利用shell的I/O进行输出重定向
• 在Linux下,文本流和二进制流是相同的
• 文本流是由文本行组成的序列,以’\n’结尾(或有回车符换行符
代替),二进制流由未经处理的字节组成
• 流与文件的连接
• 打开一个流,该流将与一个文件连接起来,关闭流则断开连接
• 指向该打开文件的指针记录了控制该流的信息
• 程序执行时, 默认会打开stdin、 stdout和stderr三个文件
• 重定向符
• 输出重定向: >、 >>、 >!
• 输入重定向: <
- >:将输出写入文件,覆盖原有内容。
- >>:将输出追加到文件,不覆盖原有内容。
- <:将文件内容作为程序的输入。
- >!:在某些shell中用于强制覆盖文件(但现代shell通常不需要)。
• 标准错误重定向
• 2>:标准错误重定向到一个文件,并覆盖原来的文件(b-shell)
• 2>>:标准错误重定向到一个文件(追加)。 1>默认为>
• 2>&1:将标准错误重定向到标准输出
• >&:将一个输出重定向到另一个句柄的输入中
• 为什么要进行重定向
• 将屏幕输出的重要信息保存下来
• 有时候不希望打印干扰屏幕
• 正确和错误的信息需要分别输出时
• 使用freopen重定向输入输出流
• 输出重定向
• 错误重定向