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

Linux硬核调试新招:延迟打印,能记录崩溃前的日志的新方法

在嵌入式开发这行,调试绝对是个绕不过去的坑。不少老哥习惯在代码里甩几个 printf,盯着串口日志找问题。这招简单好用,但要碰上硬实时系统,printf 可就成了定时炸弹。格式化字符串那点耗时,够你错过关键deadline,bug没抓到,系统先跪了。更别提那些资源紧张的小板子,printf 的开销简直是程序员的噩梦。

咋整?有哥们儿会说,用 事件缓冲区 啊!直接把事件ID和几个变量塞进数组,速度快到飞起。可这玩意儿的可读性,啧啧,基本告别人类。盯着二进制数据,手动解码,脑补上下文,简直自虐。今天咱就聊个神器—— 延迟打印,兼顾 printf 的方便和事件缓冲区的速度,堪称调试界的救命稻草。用纯C语言实现,代码硬核,效果拉满,调板子从此不慌。


延迟打印的套路

延迟打印的思路很简单:别在运行时费劲格式化字符串,直接把 printf 的参数(格式化字符串地址、参数值)原封不动塞进缓冲区,等事后再用工具把日志还原成人类能看懂的格式。就像炒菜前先把食材囤好,饿了再下锅,省时省力。

具体咋干?程序运行时,缓冲区里存的数据结构长这样:

  • 格式化字符串的地址

  • 参数个数

  • 参数0

  • 参数1

  • ……

只要格式化字符串是常量(比如代码里的字符串字面量),这套方案就稳如老狗。存数据时开销小到忽略不计,读日志时还能还原出 printf 的效果,简直是嵌入式调试的梦中情人。


读日志:从二进制到人类能看懂的文字

要从缓冲区还原日志,第一步得从可执行文件里把格式化字符串抠出来。咋抠?硬核点的可以直接解析 ELF 文件,里面有字符串的地址和内容,Linux 下 include <elf.h> 就能看到 ELF 结构体的定义,自己写个解析器不在话下。

但咱何必这么费劲?有现成的神器——gdb 了解一下!在 gdb 里敲个 print (char*)0x8045008,字符串立马蹦出来。如果地址没给对,可能会吐一堆乱码,但只要地址靠谱,效果杠杠的。咱可以用 Python 脚本控制 gdb,批量把缓冲区里的字符串读出来。

来看段代码:

charp = gdb.lookup_type('char').pointer()
def read_string(ptrbits):return gdb.Value(ptrbits).cast(charp).string()

这代码是不是有点糙?确实,Python 搞 gdb 扩展总有种“硬凑”的感觉。你也可以偷懒写:

gdb.parse_and_eval('(char*)0x%x'%ptrbits).string()

但 parse_and_eval 这货慢得像蜗牛,批量处理日志能把你急死。所以还是老老实实用第一种,效率高,值回票价。

有了 read_string,咱就能把缓冲区里的二进制数据(假设是 32 位整数数组)转成人类能读的文本:

def print_messages(words):i = 0while i < len(words):fmt = read_string(words[i])  # 读格式化字符串n = words[i+1]              # 参数个数args = words[i+2:i+2+n]     # 取参数print fmt % convert_args(fmt, args),i += n + 2

这代码干啥?就是循环读缓冲区,每次取出格式化字符串地址、参数个数和参数列表,调用 printf 格式化输出。简单粗暴,效果一流。

你可能问:convert_args 是啥?因为缓冲区里存的都是整数,%d 或 %x 直接用没问题,但 %s 和 %f 得特殊处理。%s 是个字符串地址,还得再 read_string 一次;%f 则是把整数的位重新解释成浮点数(C 里常见的 (float)&int_var 套路)。代码大概这样:

def convert_args(fmt, args):types = [s[0] for s in fmt.split('%')[1:]]  # 提取格式化类型unpack = {'s': read_string, 'd': int, 'x': int, 'f': to_float}return tuple([unpack[t](a) for t, a in zip(types, args)])def to_float(floatbits):return struct.unpack('f', struct.pack('I', floatbits))[0]

这实现有点简陋,比如不支持 %.2f 这种花式格式,但核心思路有了,实际用时再加点料就行。

最后,咱把这堆逻辑封装成 gdb 自定义命令 dprintf:

class dprintf(gdb.Command):def __init__(self):gdb.Command.__init__(self, 'dprintf', gdb.COMMAND_DATA)def invoke(self, arg, from_tty):bytes = open(arg, 'rb').read()def word(b): return struct.unpack('i', b)words = [word(bytes[i:i+4]) for i in range(0, len(bytes), 4)]print_messages(words)dprintf()

敲个 dprintf mylog.raw,日志文件里的二进制数据立马变回 printf 风格的输出。想更省事?直接命令行跑:

gdb -q prog -ex 'py execfile("dprintf.py")' -ex 'dprintf mylog.raw' -ex q

把 dprintf.py 塞进 gdb 初始化脚本,命令还能更短,爽到飞起。


写日志

存日志这块,核心是把 printf 的参数快速塞进缓冲区。C++11 的变参模板确实优雅,但咱今天玩纯C,va_list 就是最佳选手。虽然用起来有点繁琐,但性能杠杠的,线程安全也不在话下。

来看核心实现:

typedef int32_t dcell;
typedefstruct {dcell *start;  // 缓冲区起始地址dcell *curr;   // 当前写入位置dcell *finish; // 缓冲区结束地址
} dbuf;dbuf g_dbuf;  // 全局缓冲区,简单粗暴void dprintf(const char *fmt, ...) {va_list args;int n = 0;dcell *p;// 计算参数个数constchar *s = fmt;while (*s) {if (*s++ == '%') {if (*s && *s != '%') n++;  // 跳过 %% 的情况if (*s) s++;}}// 原子操作分配缓冲区空间p = __sync_fetch_and_add(&g_dbuf.curr, (n + 2) * sizeof(dcell));if (p + n + 2 > g_dbuf.finish) return;  // 缓冲区溢出,啥也不干// 存格式化字符串地址和参数个数*p++ = (dcell)fmt;*p++ = n;// 存参数va_start(args, fmt);for (int i = 0; i < n; i++) {*p++ = va_arg(args, dcell);  // 直接当整数存}va_end(args);
}

这代码咋样?全局缓冲区 g_dbuf 简单粗暴,用 __sync_fetch_and_add 搞原子操作,线程安全还能从中断里调用。啥?你说中断里用锁?那可不行,锁被挂起的线程卡住,中断直接傻眼。无锁缓冲区才是王道,忙活的线程永远不会卡别人。

参数个数咋算?咱简单遍历格式化字符串,遇到 % 就加一(跳过 %% 的特殊情况)。这实现有点糙,比如不支持复杂格式,但够用。实际项目里可以再优化,比如预先缓存格式化字符串的解析结果。

参数咋存?用 va_list 依次取出来,直接当整数塞进缓冲区。%s 和 %f 的处理得靠读日志时的 convert_args 补救,存的时候一视同仁,全当整数处理,简单又高效。

最后加个 dflush 把缓冲区刷到文件:

void dflush(void) {FILE *f = fopen("mylog.raw", "wb");if (f) {fwrite(g_dbuf.start, sizeof(dcell), g_dbuf.curr - g_dbuf.start, f);fclose(f);}g_dbuf.curr = g_dbuf.start;  // 重置缓冲区
}

用起来贼简单:

int i;
for (i = 0; i < 10; i++) {dprintf("i=%d i/2=%f %s\n", i, i/2.0, (i&1) ? "odd" : "even");
}
dflush();

崩溃前的救命稻草

日志系统牛不牛,关键看它能不能在程序崩之前把最后几条消息留下来。普通 printf 没刷盘,核心转储(core dump)里啥也看不到,急死人。延迟打印就不一样,缓冲区里的数据随时可以读。

假设程序崩了:

dprintf("going to crash...\n");
volatile int *p = 0;
dprintf("i=%d p=0x%x\n", i, (dcell)p);
p[i] = 0;  // boom!

咱扩展下 dprintf 命令,让它直接从 g_dbuf 读数据:

intpp = gdb.lookup_type('int').pointer().pointer()def invoke(self, arg, from_tty):if arg:words = read from open(arg)...else:  # 没给文件,读 g_dbufbuf = gdb.parse_and_eval('&g_dbuf').cast(intpp)start = buf[0]curr = buf[1]n = curr - startwords = [int(start[i]) for i in range(n)]print_messages(words)

为啥把 &g_dbuf 当 int** 处理?因为直接访问 buf['start'] 得靠 DWARF 调试信息(编译时加 -g)。但咱这招直接用 ELF 符号表,裸奔(没 -g)的 release 版本也能用,硬核!

核心转储里跑:

gdb -q progname core -ex dprintf -ex q

实时调试直接敲 dprintf,立马看到:

going to crash...
i=10 p=0x0

这波操作,简直是崩溃现场的救命神器。

相关文章:

  • PyQt5基本窗口控件(QWidget)
  • 使用FastAPI和React以及MongoDB构建全栈Web应用04 MongoDB快速入门
  • 【小记】excel vlookup一对多匹配
  • adb 实用命令汇总
  • 路由重发布
  • 在UniApp中css实现蚂蚁森林点击抖动效果的完整指南
  • [Linux]多线程(二)原生线程库---pthread库的使用
  • JVM——即时编译器的中间表达形式
  • LVGL图像导入和解码
  • vllm笔记
  • 《基于人工智能的智能客服系统:技术与实践》
  • Python 包管理新选择:uv
  • 栈溢出攻击最基本原理
  • MySQL 1366 - Incorrect string value:错误
  • 采用SqlSugarClient创建数据库实例引发的异步调用问题
  • 动态规划:最长递增子序列
  • Python企业级OCR实战开发:从基础识别到智能应用
  • JMeter 中实现 双 WebSocket(双WS)连接
  • 前端EXCEL插件,智表ZCELL产品V3.0 版本发布,底层采用canvas全部重构,功能大幅扩展,性能极致提升,满足千万级单元格加载
  • openlayers利用已知的三个经纬度的坐标点 , 绘制一个贝塞尔曲线
  • 上海市委常委会会议暨市生态文明建设领导小组会议研究基层减负、生态环保等事项
  • 习近平出席俄罗斯纪念苏联伟大卫国战争胜利80周年庆典
  • 新疆维吾尔自治区乌鲁木齐市米东区政协原副主席朱文智被查
  • 七大交响乐团在沪“神仙斗法”,时代交响奏出何等时代新声
  • 俄罗斯今日将举行“胜利日”阅兵,有何看点?
  • 晶圆销量上升,中芯国际一季度营收增长近三成,净利增超1.6倍