概念:
更容易构建意味着更容易测试和修改;更容易测试和修改意味着更快乐的程序和更快乐的程序员。
Make的几个重要功能:
默认是第一个目标target
隐式编译规则
例如,将.c文件编译成.o文件。根据惯例,该隐含规则使用变量CFLAGS来定义C编译标志。
变量
CFLAGS=-Wall -g
,CC
特殊变量
makefile使用特殊变量$@
,这个特殊变量表示 "目标"。特殊变量可以避免不必要的输入,并使规则更容易改变和重复使用。
包含一个main程序和2个lib项目的Makefile:
CC=gcc
CFLAGS=-Wall -g
LIBS=lib.o lib2.o
HEADERS=lib.h lib2.h
BINS=prog lib_test lib2_test
all: ${BINS}
prog: prog.o ${LIBS} ${HEADERS}
${CC} -o $@ ${CFLAGS} prog.o ${LIBS}
lib_test: lib.o lib_test.o lib.h tests.o tests.h
${CC} -o $@ ${CFLAGS} lib_test.o lib.o tests.o
lib2_test: lib2.o lib2_test.o lib2.h tests.o tests.h
${CC} -o $@ ${CFLAGS} lib2_test.o lib2.o tests.o
test:
./lib_test
./lib2_test
clean:
rm -rf ${BINS} *.o core *.core
编译器检查
总是使用gcc -Wall
用**-D_FORTIFY_SOURCE
**及早发现错误
用-D_FORTIFY_SOURCE=2
编译程序将包括对系统调用(如printf)的缓冲区对齐的额外检查。这些检查决定了你是否向sprintf等函数传递了一个太小的缓冲区。
使用glibc变量及早发现错误
设置环境变量 MALLOC_CHECK_
来识别 malloc 相关的问题,如果设置为1会记录错误,设置为2会导致立即终止。
一些有用的调试步骤:
发现bug后需要思考的是:
调试宏可以在程序中增加调试printfs而不用移除。一组好的调试宏可以有选择的启用和关闭不同级别的调试级别。通常指定输出信息的方式有两种:调试级别和指定调试的功能(socket 操作,进程相关)。
#ifndef _DEBUG_H_
#define _DEBUG_H_
#include <stdio.h> /* for perror */
#include "err.h"
#ifdef DEBUG
extern unsigned int debug;
#define DPRINTF(level, fmt, args...) \
do { \
if ((debug) & (level)) fprintf(stderr, fmt, ##args); \
} while (0)
#define DEBUG_PERROR(errmsg) \
do { \
if ((debug)&DEBUG_ERRS) perror(errmsg); \
} while (0)
#define DEBUGDO(level, args) \
do { \
if ((debug) & (level)) { \
args; \
} \
} while (0)
#else
#define DPRINTF(args...)
#define DEBUG_PERROR(args...)
#define DEBUGDO(args...)
#endif
/*
* Debug levels
*/
#define DEBUG_NONE 0x00 // DBTEXT: No debugging
#define DEBUG_ERRS 0x01 // DBTEXT: Verbose error reporting
#define DEBUG_INIT 0x02 // DBTEXT: Debug Initialization
#define DEBUG_SOCKETS 0x04 // DBTEXT: Debug sockets operations
#define DEBUG_PROCESSES 0x08 // DBTEXT: Debug processes operations fork/reap
#define DEBUG_ALL 0xffffffff // DBTEXT: All debugging
#ifdef __cplusplus
extern "C" {
#endif
int set_debug(char *arg); /* returns 0 on success, -1 on failure */
#ifdef __cplusplus
}
#endif
#endif /* _DEBUG_H_ */
工作方式:
常见功能:
说明:
使用 ulimit -a
查看资源限制,如果 coredumpsize 的值被设置为零,核心转储将被禁用。可以使用:ulimit -c unlimited
启用,非必要时可以使用 ulimit -c 0
禁用。
Linux下core dump文件通常命名为 core/core.PID;Mac下会存储在 /cores 目录。
使用系统调用追踪工具 ktrace(BSD)、strace(Linux).
strace
strace产生的输出列出了每个系统调用、其参数和结果。strace输出对于观察程序崩溃前的最后几个系统调用是非常有用的。
Valgrind是一个动态分析工具(它在你的代码运行时测试你的代码)。它提供的工具可以检查无效或未初始化的内存使用情况,在内存被释放后进行写入,以及内存泄漏。由于它在虚拟机中运行程序,程序在Valgrind下的运行速度要慢10倍左右。Valgrind只在Linux上可用。
valgrind
调试缓冲区越界的工具。它监控对malloc生成的缓冲区的访问,以检测你的代码是否写过了缓冲区的末端(或开始)。
ef
或者可以使用链接 -lefence
来使用。使用electricfence,试图访问分配内存外的部分将导致程序出现seg故障,而不是引起奇怪的、不可预知的行为。
对于网络项目和分布式系统的调试,数据包嗅探器如tcpdump和Wireshark可以说是非常有用的。这些工具可以记录进出机器的数据包,并可以解码各种协议。
对输出进行 "比较 "往往是有帮助(例如文件传输,结果比较)
让代码功能显而易见/明显,不要注释明显的代码,尽量保持干净。
重要的是选择一种风格,并一致地使用它。并且满足:
统一使用风格比细节要重要得多。也就是说,随着时间的推移,一些约定俗成的东西已经出现了,它们可以极大地帮助使代码更加可读。例如:
感受编码风格和技巧的一个好方法是阅读一些其他代码。建议阅读BSD源代码中的一些系统实用程序。总的来说,它组织得很好,很一致,而且经得起时间的考验。一些建议:
时间是宝贵的,将重复性无意义的工作自动化的最好方法是使用脚本语言,如Perl、Python和Ruby。
大多数脚本语言都有对 "单行 "的某种支持,可以完成特定的任务,例如将一组文件中出现的 "foo "改为 "bar":
perl: perl -pi.bak -e "s/foo/bar/g”
ruby: ruby -pi.bak -e "gsub(’foo’, ’bar’)”
https://wiki.python.org/moin/Powerful Python One-Liners
https://www.bashoneliners.com/oneliners/popular/
掌握shell常见功能,通配符,循环,重定向等等。
以及命令行、面向管道的工具,如 "awk "和 "sed"。
程序设计有两个关键的方面是需要解决的:
如何将程序中的功能分解成单个的源代码片段,例如:
不要重复自己的工作DRY
DRY是决定哪些代码应该被拉入模块的经验法则。如果你发现自己不断重复相同或几乎相同的代码,或者更糟糕的是复制和粘贴它!那么是时候考虑把它分离成一个漂亮、干净的模块,可以在其他地方使用。
隐藏不必要的实现细节
会使以后改变这些细节更容易,而不需要改变所有使用模块的代码。
保持简单!
不要过早地进行优化,不要添加不必要的功能,设计可重用的东西。
极不可能坐下来,在一次不间断的狂欢中,完成整个工作项目。
相反,在开始编码之前,坐下来思考一下你可以沿途采取的渐进步骤。在开发时,以这些渐进的步骤为目标,这样你就可以把问题分解成小的、可管理的块状。你的目标应该是,一旦你开发了一个特定的 "块",你就可以相信它此后会继续做它的工作。通过如下方法进行分解:
在设计和编码过程中测试的第一个目标是确保一旦你建立了一个特定的组件或模块,你可以依靠这个组件来工作,并保持工作。后来,当你的程序出现错误时,你就可以专注于较新的代码,减少了调试和后期编码的复杂性。
单元测试是测试的最基本元素。它们特别适合于你在前面将代码分解成步骤时确定的那种完善的模块-实用程序、容器等。对于这些模块中的每一个,我们建议写一套专门针对该模块的测试。编写测试用例可以使你更努力地思考你所暴露的接口和你必须实现的行为。
系统测试是对大局的测试,注意,即使在这里,你也可以从项目的开始就开始写有用的测试。
在设计测试的时候,要把自己放在真正的、真正的试图破解代码的心态上。不要只测试简单的情况,要测试边界情况。
先是简单的测试,然后是复杂的测试。很可能在你第一次尝试简单的测试时,你会发现一些错误。一旦你解决了这些问题,就可以继续进行更复杂的测试,以发现更难解决的问题。
通过这样做,你将更经常地运行它们,将对你更有用。理想情况下,你应该能够类似使用 make test 让你的所有测试运行。
了解你的编辑器并对其了如指掌,可以为你节省无数的按键和零星的时间。常见特点:
请注意,当调试涉及多个进程或机器或操作系统内核代码时,一些IDE的集成特性在系统编程的背景下可能对你不利。
本文链接: Software Engineering for Systems Hackers
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
发布日期: 2022-04-30
最新构建: 2024-12-26
欢迎任何与文章内容相关并保持尊重的评论😊 !