在Linux下优雅的调试C语言
最近在开发ruri时遇到不少问题,咱也是第一次写C,早知道头顶这么发凉就去用某邪教了呜喵~
至于学习C语言的心得嘛,
1 | 陷入无法察觉的overflow |
(高速退学)
好了好了,C语言还是有许多优点的,只是可能入门成本高些罢了,如果善用测试工具的话还是没有那么糟糕的,话不多说我们开始今天的正文。
首要前提:
代码没bug的就不要调试了,编程第一法则不就是能跑的代码不要动嘛喵~
过早的优化是万恶之源,测试时不要开-O2,且尽量使用-O0 -fno-omit-frame-pointer -z norelro -z execstack -no-pie -fno-stack-protector -Wall -Wextra -pedantic -Wconversion
来测试。
至于O3。。。除非编码特别规范否则几乎是炸屎。
那如果有bug呢?
首先得能过编译器,编译器都报error的代码再高端的调试工具也无能为力。
然后检查编译器的警告,加上参数-Wall -Wextra
编译然后查看警告,若是编译器警告都无法修复的话。。。这bug咱还是别修了吧喵~
如果编译器不报警呢?
于是就是今天的主题了–如何面对编译时无法找出的bug。
消极面对:
部分内存问题可以通过编译器参数被隐藏,编译时加上-fPIE -z noexecstack -z now -fstack-protector-all -fstack-clash-protection -mshstk -O2
说不定就能跑了喵~
好了本文完,下期再见~
桥豆麻袋,自己的项目中的代码肯定不能挖坑埋雷啊喵~
积极面对:
中国有句古话叫做,食食物者为俊杰,眼下的各种工具,我想一定能找到阁下的bug。
使用clang-tidy检查代码
clang-tidy是llvm项目的一部分,用于代码静态检测。
由于clang-tidy过于优秀,大部分简单的bug在这里就会被检测出来,根本用不到真的运行,当然,它无法检查代码的功能是否可以正确实现,所以偶尔也必须得上gdb。
基本用法:
1 | clang-tidy xxx.c -- 编译参数 |
注意编译参数前的--
,后面接clang/gcc编译时的参数。
但是,很多规则不是有用的,比如对strlen.h中函数内存安全的报警就非常多余,甚至clang-tidy会建议使用BSD中的函数替代,对此咱建议还是不要建议了。
因此我们需要关闭部分检测项目。
使用--checks=-检测项
来关闭检测项。
建议关闭的检测项:
1 | --checks=-clang-analyzer-security.insecureAPI.strcpy,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling |
这样大部分内存泄漏等问题都可以被找出了。
如果需要更多检测:
1 | --checks=* |
比如ruri中的make check
:
1 | --checks=*,-clang-analyzer-security.insecureAPI.strcpy,-altera-unroll-loops,-cert-err33-c,-concurrency-mt-unsafe,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-readability-function-cognitive-complexity,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-bugprone-easily-swappable-parameters |
目前ruri已经过了这些检测,但愿读者的内存永远不要泄漏喵~
由于clang-tidy检测项太多,有些是对理解难度甚至是对todo风格等的检查,有些检查根本没法满足比如函数使用嵌套也会有警告,因此检查时需要我们手动对我们认为无效的警告进行过滤。
一些有用的警告:
数组越界
内存泄漏
未初始化的指针
free()后还在使用的内存或者使用后没被free()的内存
未初始化的变量
void函数最后的return
使用ASAN查看内存问题:
ASAN全称Address Sanitizer,是google发明的一种内存地址错误检查器,用于在运行时检测代码内存问题。
如何使用:
编译时加入参数-O0 -fsanitize=address -fno-stack-protector -fno-omit-frame-pointer
如果运气好的话,设置环境变量LSAN_OPTIONS="verbosity=1:log_threads=1" ASAN_OPTIONS="verbosity=1"
,你将看到一片fa的冥场面。
1 | SUMMARY: AddressSanitizer: heap-buffer-overflow asan_interceptors.cpp.o in printf_common(void*, char const*, __va_list_tag*) |
貌似还…….挺好看……
一般来讲出问题的行会在后面汇报。
注意:建议使用clang-tidy检查有bug的代码,因为一旦内存有问题的话很可能程序不会在出问题那行崩溃。
ASAN面对fork()后的程序貌似有点抽风,ruri好不容易跑起来了,结果退出时卡在sched_yield()
这个系统调用,欺负萌新是吧呜呜呜~
在有些教程中ASAN偶尔会配合addr2line使用,实测貌似也定位不到相关行,或许是咱太笨了喵~
使用GDB调试工具
GDB全称The GNU Project Debugger,是GNU项目的一部分。建议使用来检测代码是否实现而非内存问题,除非clang-tidy无法检测出来,在面对踩内存/带goto的代码时用GDB可能会十分痛苦。
在编译时加如参数-ggdb
,不要开任何优化,然后就可以使用gdb来调试程序了。
注意,代码里少写两个goto有助于调试,白皮书说C语言提供了可以随意滥用的goto语句,瞧瞧这说的,像话吗喵!!!
注意,请先使用clang-tidy检查是否有leak of memory等内存问题,否则你可能会遇上这种冥场面:
1 | Breakpoint 2, check_container ( |
咱甚至还在未申请到内存的结构体中读到过一个ELF,估计是指针指向程序头之类的地方了。
基本命令:
1 | gdb ./可执行文件 |
或者对于运行中的程序:
1 | gdb attach <pid> |
回车同意协议,然后你获得了一个这样的终端:
1 | For help, type "help". |
基本命令:
开始运行程序:
1 | r 程序的命令行参数 |
设置断点:
1 | b 行号 |
继续执行:
1 | c |
追踪子进程:
1 | set follow-fork-mode child |
查看当前行号:
1 | where |
查看上面的行:
1 | up |
打印变量:
1 | p 变量名/表达式 |
这个功能真的震惊到咱了,因为C语言表达式都能用。
比如:
1 | (gdb) p container_info->container_dir |
监控变量:
1 | watch 变量名 |
一个特殊的breakpoint:
1 | 在出口处: |
GDB插件推荐:peda
peda(Python Exploit Development Assistance for GDB)是隔壁PWN大佬们的玩具,虽然咱看不懂汇编,不过其自动打印信息的功能还是很有帮助的。并且最重要的是它很帅啊!!!
看下对比:
1 | [----------------------------------registers-----------------------------------] |
不带peda:
1 | Breakpoint 1, 0x00007ffff7dd0df0 in exit () from /usr/lib/libc.so.6 |
最起码有种黑客的感觉喵~
使用strace工具:
strace全称Linux Syscall Tracer,听名字就知道,用于追踪进程的系统调用。众所周知,进程总要有系统调用,追踪这部分内容有时可以帮助我们发现问题。
基本用法:
1 | 对于已有进程: |
所以咱的程序在ASAN下卡在sched_yield() = 0
是为什么啊喵!!!
总结:
C语言虽然很容易写出bug,但是善用工具,养成良好的代码风格还是可以避免大部分问题的。还有就是,得会点英语。
群里曾经有一位萌新问道:
“如果我想入门编程语言,学哪种比较好?”
大佬答:”英语”
本文完,我们下期再见喵~
EOF