Unique's Blog

Software Engineering for Systems Hackers

2022-04-30 · 3145字 · 12 min read
🏷️  Article C++

版本控制

概念:

  • 主副本存储在版本库中
  • 每个版本都是可用的
  • 多开发分支
  • 并行开发

构建工具和自动化

更容易构建意味着更容易测试和修改;更容易测试和修改意味着更快乐的程序和更快乐的程序员。

Makefiles

Make的几个重要功能:

  • 默认是第一个目标target

  • 隐式编译规则

    例如,将.c文件编译成.o文件。根据惯例,该隐含规则使用变量CFLAGS来定义C编译标志。

  • 变量

    CFLAGS=-Wall -gCC

  • 特殊变量

    makefile使用特殊变量$@,这个特殊变量表示 "目标"。特殊变量可以避免不必要的输入,并使规则更容易改变和重复使用。

Example

包含一个main程序和2个lib项目的Makefile:

  • prog.c: The main program file
  • lib.c: Lib#1 c file
  • lib.h: Lib#1 header file
  • lib_test.c: a set of tests for Library #1
  • lib2.c: Lib#2 c file
  • lib2.h: Lib#2 header file
  • lib2_test.c: a set of tests for Library #2
  • tests.h: A header file for some common test functions
  • tests.c: A library with some useful test routines
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后需要思考的是:

  1. 是否在代码的其他地方犯了这个错误?由于对接口的误解而产生的bug,很可能会在多个地方出现。做一次快速检查,以主动消除其他bug。
  2. 怎样才能避免在将来犯这个错误?你也许可以添加测试用例来自动发现它们,在代码中添加断言来检测该错误是否再次发生,使用编译器警告来自动检测它们,或者改变你写代码的方式,使其不可能犯错。

Printf:调试宏

调试宏可以在程序中增加调试printfs而不用移除。一组好的调试宏可以有选择的启用和关闭不同级别的调试级别。通常指定输出信息的方式有两种:调试级别和指定调试的功能(socket 操作,进程相关)。

debug.h
#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_ */

调试工具

gdb

工作方式:

  • gdb binary 调试二进制程序
  • gdb binary core 调试崩溃程序的core文件
  • gdb binary PID 附加到一个正在运行的进程

常见功能:

ifg

说明:

  1. 没有core文件?

使用 ulimit -a 查看资源限制,如果 coredumpsize 的值被设置为零,核心转储将被禁用。可以使用:ulimit -c unlimited 启用,非必要时可以使用 ulimit -c 0 禁用。

Linux下core dump文件通常命名为 core/core.PID;Mac下会存储在 /cores 目录。

追踪系统调用

使用系统调用追踪工具 ktrace(BSD)、strace(Linux).

strace [args]

strace产生的输出列出了每个系统调用、其参数和结果。strace输出对于观察程序崩溃前的最后几个系统调用是非常有用的。

使用Valgrind内存调试

Valgrind是一个动态分析工具(它在你的代码运行时测试你的代码)。它提供的工具可以检查无效或未初始化的内存使用情况,在内存被释放后进行写入,以及内存泄漏。由于它在虚拟机中运行程序,程序在Valgrind下的运行速度要慢10倍左右。Valgrind只在Linux上可用。

valgrind [args]

使用Electric Fence内存调试

调试缓冲区越界的工具。它监控对malloc生成的缓冲区的访问,以检测你的代码是否写过了缓冲区的末端(或开始)。

ef [args]

或者可以使用链接 -lefence 来使用。使用electricfence,试图访问分配内存外的部分将导致程序出现seg故障,而不是引起奇怪的、不可预知的行为。

使用tcpdump/Wireshark追踪网络包

对于网络项目和分布式系统的调试,数据包嗅探器如tcpdump和Wireshark可以说是非常有用的。这些工具可以记录进出机器的数据包,并可以解码各种协议。

  • tcpdump是一个更低级的原始数据包接口;它很容易从命令行使用,并产生有用的输出;
  • Wireshark能解析更多的协议,有更强大的功能(如重新组合TCP流),并且有一个方便的GUI

测试输出

对输出进行 "比较 "往往是有帮助(例如文件传输,结果比较)

文档和风格

让代码功能显而易见/明显,不要注释明显的代码,尽量保持干净。

风格

重要的是选择一种风格,并一致地使用它。并且满足:

  • 使代码可读性强
  • 能区分重要的东西,如C++中的类名、变量、全局变量等
  • 命名不要过分冗长和痛苦

统一使用风格比细节要重要得多。也就是说,随着时间的推移,一些约定俗成的东西已经出现了,它们可以极大地帮助使代码更加可读。例如:

  • IsValid()是一个比checkValid()更好的名字,因为其语义更清晰
  • 在介绍函数时要有简短的注释,解释其语义任何调用者真正需要知道的关于使用它的信息

阅读其他代码

感受编码风格和技巧的一个好方法是阅读一些其他代码。建议阅读BSD源代码中的一些系统实用程序。总的来说,它组织得很好,很一致,而且经得起时间的考验。一些建议:

  • ed 测试代码
  • 很多内核代码很有趣,但很密集

使用脚本

时间是宝贵的,将重复性无意义的工作自动化的最好方法是使用脚本语言,如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

掌握shell常见功能,通配符,循环,重定向等等。

以及命令行、面向管道的工具,如 "awk "和 "sed"。

程序设计

程序设计有两个关键的方面是需要解决的:

  • 数据结构。该程序对什么数据进行操作?它是如何存储的
  • 模块。程序是如何被划分为各个组件的?这些组件是如何交互的?

如何将程序中的功能分解成单个的源代码片段,例如:

  1. 不要重复自己的工作DRY

    DRY是决定哪些代码应该被拉入模块的经验法则。如果你发现自己不断重复相同或几乎相同的代码,或者更糟糕的是复制和粘贴它!那么是时候考虑把它分离成一个漂亮、干净的模块,可以在其他地方使用。

  2. 隐藏不必要的实现细节

    会使以后改变这些细节更容易,而不需要改变所有使用模块的代码。

  3. 保持简单!

    不要过早地进行优化,不要添加不必要的功能,设计可重用的东西。

为渐进式的快乐而设计

极不可能坐下来,在一次不间断的狂欢中,完成整个工作项目。

相反,在开始编码之前,坐下来思考一下你可以沿途采取的渐进步骤。在开发时,以这些渐进的步骤为目标,这样你就可以把问题分解成小的、可管理的块状。你的目标应该是,一旦你开发了一个特定的 "块",你就可以相信它此后会继续做它的工作。通过如下方法进行分解:

  • 识别单独的构件(列表、哈希、算法等)
  • 识别 特征/基本本质

可测试性设计

在设计和编码过程中测试的第一个目标是确保一旦你建立了一个特定的组件或模块,你可以依靠这个组件来工作,并保持工作。后来,当你的程序出现错误时,你就可以专注于较新的代码,减少了调试和后期编码的复杂性。

单元测试是测试的最基本元素。它们特别适合于你在前面将代码分解成步骤时确定的那种完善的模块-实用程序、容器等。对于这些模块中的每一个,我们建议写一套专门针对该模块的测试。编写测试用例可以使你更努力地思考你所暴露的接口和你必须实现的行为。

系统测试是对大局的测试,注意,即使在这里,你也可以从项目的开始就开始写有用的测试。

对抗的心态

在设计测试的时候,要把自己放在真正的、真正的试图破解代码的心态上。不要只测试简单的情况,要测试边界情况。

先是简单的测试,然后是复杂的测试。很可能在你第一次尝试简单的测试时,你会发现一些错误。一旦你解决了这些问题,就可以继续进行更复杂的测试,以发现更难解决的问题。

测试自动化

通过这样做,你将更经常地运行它们,将对你更有用。理想情况下,你应该能够类似使用 make test 让你的所有测试运行。

编辑器

了解你的编辑器并对其了如指掌,可以为你节省无数的按键和零星的时间。常见特点:

  • 语法高亮。对运算符、函数调用、注释等进行着色,可以使你更容易扫描代码,找到你要找的东西
  • 标签。一些编辑器和集成开发环境(IDE)提供了单次点击或按键命令,让你快速跳转到声明或实现某个函数的文件中。
  • 自动补全。编辑器提供不同类型的自动完成功能,从括号匹配到函数名称自动完成,等等。

请注意,当调试涉及多个进程或机器或操作系统内核代码时,一些IDE的集成特性在系统编程的背景下可能对你不利。

阅读

https://www.cs.cmu.edu/~dga/systems-se.pdf

本文链接: Software Engineering for Systems Hackers

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

发布日期: 2022-04-30

最新构建: 2024-12-26

本文已被阅读 0 次,该数据仅供参考

欢迎任何与文章内容相关并保持尊重的评论😊 !

共 43 篇文章 | Powered by Gridea | RSS
©2020-2024 Nuo. All rights reserved.