24、C 语言编程:程序构建、调试与分析

C 语言编程:程序构建、调试与分析

在 C 语言编程中,我们不仅要编写功能代码,还需要掌握程序构建、调试和分析的技巧,以确保程序的正确性、安全性和性能。下面将详细介绍相关内容。

程序结构与构建

在编写一个判断质数的程序时,我们需要完成一系列的步骤来构建程序。

首先是 main 函数的实现,它调用 convert_cmd_line_args 函数将命令行参数转换为无符号长整型数组,然后使用 is_prime 函数(基于 Miller - Rabin 素性测试)来判断每个参数是否为质数。以下是 main 函数的部分代码:

if (!vals) return EXIT_FAILURE;
for (size_t i = 0; i < num_args; ++i) {
    printf("%llu is %s.\n", vals[i],
           is_prime(vals[i], 100) ? "probably prime" : "not prime");
}
free(vals);
return EXIT_SUCCESS;

接下来是构建代码的步骤:
1. 创建文件
- 创建 isprime.c 文件,包含 Listings 10 - 8 10 - 9 的代码,并在文件顶部添加 #include "isprime.h" #include <stdlib.h>
- 创建 isprime.h 头文件,提供静态库的公共接口,代码如下:

#ifndef PRIMETEST_IS_PRIME_H
#define PRIMETEST_IS_PRIME_H
bool is_prime(unsigned long long n, unsigned k);
#endif // PRIMETEST_IS_PRIME_H
- 创建 `driver.c` 文件,包含 `Listings 10 - 5`、`10 - 6`、`10 - 7` 和 `10 - 10` 的代码,并在文件顶部添加 `#include "isprime.h"`、`#include <assert.h>`、`#include <errno.h>`、`#include <limits.h>`、`#include <stdio.h>` 和 `#include <stdlib.h>`。
- 创建本地目录 `bin`,用于存放构建产物。
  1. 编译源文件 :使用 Clang 或 GCC 编译器将 C 源文件编译为目标文件,存放在 bin 目录中。
% cc -c -std=c23 -Wall -Wextra -pedantic isprime.c -o bin/isprime.o
% cc -c -std=c23 -Wall -Wextra -pedantic driver.c -o bin/driver.o

对于较旧的编译器,可能需要将 -std=c23 替换为 -std=c2x 。如果出现 unable to open output file 'bin/isprime.o': 'No such file or directory' 错误,需要创建 bin 目录并重新执行命令。

  1. 创建静态库 :使用 ar 命令生成静态库 libPrimalityUtilities.a
% ar rcs bin/libPrimalityUtilities.a bin/isprime.o
  1. 链接生成可执行程序 :将 driver.o 目标文件与静态库链接,生成可执行程序 primetest
% cc bin/driver.o -Lbin -lPrimalityUtilities -o bin/primetest
程序测试

构建完成后,我们可以对程序进行测试,尝试不同的输入,如负数、已知的质数和非质数以及错误输入:

% ./bin/primetest 899180
899180 is not prime
% ./bin/primetest 8675309
8675309 is probably prime
% ./bin/primetest 0
primetest num1 [num2 num3 ... numN]
Tests positive integers for primality.
Tests numbers in the range [2 - 18446744073709551615].
断言的使用

在 C 语言中,断言是一种验证程序假设的重要工具,分为静态断言和运行时断言。

静态断言

静态断言使用 static_assert 关键字,可在编译时检查假设。以下是几个使用静态断言的例子:
- 验证结构体无填充字节

struct packed {
    int i;
    char *p;
};
static_assert(
    sizeof(struct packed) == sizeof(int) + sizeof(char *),
    "struct packed must not have any padding"
);
  • 验证整数大小
#include <stdio.h>
#include <limits.h>
void clear_stdin() {
    int c;
    do {
        c = getchar();
        static_assert(
            sizeof(unsigned char) < sizeof(int),
            "FIO34 - C violation"
        );
    } while (c != EOF);
}
  • 编译时边界检查
static const char prefix[] = "Error No: ";
constexpr int size = 14;
char str[size];
// ensure that str has sufficient space to store at
// least one additional character for an error code
static_assert(
    sizeof(str) > sizeof(prefix),
    "str must be larger than prefix"
);
strcpy(str, prefix);
运行时断言

运行时断言使用 assert 宏,在程序运行时检查假设。例如:

void *dup_string(size_t size, char *str ) {
    assert(size <= LIMIT);
    assert(str != nullptr);
    // --snip--
}

在代码部署前,可通过定义 NDEBUG 宏来禁用断言。

编译器设置和标志

不同的编译器(如 GCC、Clang 和 Visual C++)提供了各种标志来优化代码、检测错误和增强安全性。以下是不同编译器的推荐标志:

GCC 和 Clang 标志
标志 目的
-D_FORTIFY_SOURCE=2 检测缓冲区溢出
-fpie -Wl,-pie 地址空间布局随机化所需
-fpic -shared 禁用共享库的文本重定位
-g3 生成丰富的调试信息
-O2 优化代码的速度/空间效率
-Wall 开启推荐的编译器警告
-Wextra 开启更多推荐的编译器警告
-Werror 将警告转换为错误
-std=c23 指定语言标准
-pedantic 严格符合标准时发出警告
-Wconversion 警告可能改变值的隐式转换
-Wl,-z,noexecstack 将栈段标记为不可执行
-fstack-protector-strong 为函数添加栈保护
Visual C++ 选项
标志 目的
/guard:cf 添加控制流保护安全检查
/analyze 启用静态分析
/sdl 启用安全功能
/permissive- 指定编译器的标准合规模式
/O2 设置优化级别为 2
/W4 设置编译器警告级别为 4
/WX 将警告转换为错误
/std:clatest 选择最新的语言版本

通过合理使用这些编译器标志,我们可以在不同的软件开发阶段(如构建、调试、测试、优化和发布)更好地控制代码的质量和性能。

下面是一个简单的流程图,展示了程序构建的主要步骤:

graph LR
    A[创建文件] --> B[编译源文件]
    B --> C[创建静态库]
    C --> D[链接生成可执行程序]
    D --> E[测试程序]

综上所述,掌握程序构建、调试和分析的技巧对于 C 语言编程至关重要。通过合理运用静态断言和运行时断言,以及选择合适的编译器标志,我们可以编写出更安全、更高效的 C 语言程序。

C 语言编程:程序构建、调试与分析

不同软件开发阶段的编译器设置

软件开发过程包含多个阶段,每个阶段对代码的要求不同,因此需要选择合适的编译器设置。

构建阶段

构建阶段的目标是利用编译器分析尽可能消除缺陷。建议使用能最大化诊断信息的编译器选项,如 GCC 和 Clang 的 -Wall -Wextra -Werror -std=c23 -pedantic 等标志。这些标志能帮助开发者发现代码中的潜在问题,避免在后续调试和测试阶段花费更多时间。

调试阶段

调试时,我们需要确定代码无法正常工作的原因。此时,应使用包含调试信息、允许断言发挥作用且能实现快速编辑 - 编译 - 调试循环的编译器标志。例如, -O0 -g3 是一个不错的默认选择,它能生成丰富的调试信息,同时不进行优化,使机器指令与源代码紧密对应。以下是一个简单的示例:

#include <stdio.h>
#include <stdlib.h>
#define HELLO "hello world!"
int main()
{
    puts(HELLO);
    return EXIT_SUCCESS;
}

使用不同的编译选项会有不同的调试效果:
- 仅使用 -Og 编译:

$ gcc -Og hello.c -o hello
$ gdb hello
(...)
(No debugging symbols found in hello)
(gdb)
  • 使用 -Og -g 编译:
$ gcc -Og -g hello.c -o hello
$ gdb hello
(...)
Reading symbols from hello...
(gdb) break main
Breakpoint 1 at 0x1149: file hello.c, line 6.
(gdb) start
Temporary breakpoint 2 at 0x1149: file hello.c, line 6.
Starting program: /home/test/Documents/test/hello
Breakpoint 1, main () at hello.c:6
6      int main()
(gdb) print HELLO
No symbol "HELLO" in current context.
(gdb)
  • 使用 -Og -g3 编译:
$ gcc -Og -g3 hello.c -o hello
$ gdb hello
(...)
Reading symbols from hello...
(gdb) break main
Breakpoint 1 at 0x1149: file hello.c, line 6.
(gdb) start
Temporary breakpoint 2 at 0x1149: file hello.c, line 6.
Starting program: /home/test/Documents/test/hello
Breakpoint 1, main () at hello.c:6
6      int main()
(gdb) print HELLO
$1 = "hello world!"
(gdb)
测试阶段

测试阶段可以保留调试信息并启用断言,以便识别问题的根本原因。同时,可以注入运行时检测工具来帮助发现错误。

配置文件引导优化阶段

此阶段定义编译器和链接器标志,控制编译器在代码中添加运行时检测工具,收集性能统计信息,以找到程序的热点进行优化。

发布阶段

在将代码部署到生产环境之前,要确保对发布配置进行彻底测试。因为不同的编译标志可能会触发新的缺陷,如潜在的未定义行为或优化带来的时序问题。

不同编译器标志的详细分析
GCC 和 Clang 标志
  • -O 系列标志
    • -O0 :完全禁用优化,是未设置优化级别时的默认选项。
    • -Og :抑制可能影响调试体验的优化阶段。
    • -O2 :优化代码的速度和空间效率。
    • -Os :优化代码大小,启用除通常会增加代码大小的 -O2 优化之外的所有优化。
    • -Oz :积极优化代码大小,可能会增加执行的指令数量,但指令编码所需的字节数更少。在 Clang 中使用时需与 -mno-outline 结合,GCC 12.1 及更高版本支持。
  • -g 系列标志
    • -g 标志用于生成调试信息,默认级别为 -g2
    • -g3 :生成更丰富的调试信息,包括所有宏定义,允许在支持的调试器中展开宏。
    • -ggdb3 :告诉 GCC 使用最适合 GNU 项目调试器(GDB)的格式生成调试信息。
  • -Wall -Wextra
    • -Wall -Wextra 启用预定义的编译时警告集,但并非所有可能的警告诊断。可以通过 gcc -Wall -Wextra -Q --help=warning 查看具体启用的警告列表。
  • -Wconversion
    • 警告可能改变值的隐式转换,包括浮点与整数之间的转换、有符号与无符号整数之间的转换以及转换为较小类型的情况。可以使用 -Wno-sign-conversion 禁用有符号与无符号整数转换的警告,但通常这些警告有助于发现某些类型的缺陷和安全漏洞,建议保持启用。
  • -Werror
    • 将所有警告转换为错误,要求开发者在调试前解决这些问题,有助于培养良好的编程习惯。
  • -std=
    • 用于指定 C 语言标准,如 c89 c90 c99 c11 c17 c23 。对于较旧的编译器,可能需要使用 -std=c2x 。为了代码的可移植性和使用新特性,建议指定标准。
  • -pedantic
    • 当代码不符合严格的标准时发出警告,通常与 -std= 标志结合使用,以提高代码的可移植性。
  • -D_FORTIFY_SOURCE=2
    • 检测缓冲区溢出。在分析、测试和生产构建中,对于 Clang 和 GCC 12.0 之前的版本,建议使用 -D_FORTIFY_SOURCE=2 -D_FORTIFY_SOURCE=1 ;对于 GCC 12.0 及更高版本,使用 -D_FORTIFY_SOURCE=3
  • -fpie -Wl,-pie -fpic -shared
    • -fpie -Wl,-pie 用于创建位置独立的可执行程序,支持地址空间布局随机化(ASLR)。
    • -fpic -shared 用于禁用共享库的文本重定位,适用于支持位置相关共享库的架构,动态共享对象始终支持 ASLR。
  • -Wl,-z,noexecstack
    • 告诉链接器将栈段标记为不可执行,使操作系统在加载程序时配置内存访问权限,增强安全性。
  • -fstack-protector-strong
    • 通过添加栈保护机制,保护应用程序免受常见的栈缓冲区溢出攻击。
Visual C++ 选项
  • /guard:cf
    • 添加控制流保护(CFG)安全检查,检测试图破坏程序控制流的行为。
  • /analyze
    • 启用静态分析,帮助发现代码中的潜在问题。
  • /sdl
    • 启用安全功能,增强代码的安全性。
  • /permissive-
    • 指定编译器的标准合规模式,确保代码符合标准。
  • /O2
    • 设置优化级别为 2,适用于部署的代码。
  • /Od
    • 禁用优化,加快编译速度,简化调试过程。
  • /W4
    • 设置编译器警告级别为 4,大致相当于 GCC 和 Clang 的 -Wall ,适用于新代码。
  • /Wall
    • 不建议使用,会产生大量误报。
  • /WX
    • 将警告转换为错误,与 GCC 和 Clang 的 -Werror 类似。
  • /std:clatest
    • 选择最新的语言版本,以使用新的语言特性。
总结

C 语言编程不仅需要编写功能代码,还需要掌握程序构建、调试和分析的技巧。通过合理使用静态断言和运行时断言,可以验证程序的假设,提高代码的可靠性。同时,根据不同的软件开发阶段选择合适的编译器标志,可以优化代码的性能、检测错误和增强安全性。以下是一个总结的流程图,展示了整个过程:

graph LR
    A[编写代码] --> B[选择编译器标志]
    B --> C{开发阶段}
    C -->|构建| D[最大化诊断信息]
    C -->|调试| E[包含调试信息, 允许断言]
    C -->|测试| F[保留调试信息, 启用断言]
    C -->|优化| G[收集性能信息, 优化热点]
    C -->|发布| H[彻底测试发布配置]
    D --> I[编译代码]
    E --> I
    F --> I
    G --> I
    H --> I
    I --> J[使用断言验证假设]
    J --> K[测试程序]
    K --> L[部署代码]

通过遵循这些原则和方法,开发者可以编写出更安全、更高效的 C 语言程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值