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`,用于存放构建产物。
-
编译源文件
:使用 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
目录并重新执行命令。
-
创建静态库
:使用
ar命令生成静态库libPrimalityUtilities.a。
% ar rcs bin/libPrimalityUtilities.a bin/isprime.o
-
链接生成可执行程序
:将
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。为了代码的可移植性和使用新特性,建议指定标准。
-
用于指定 C 语言标准,如
-
-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。
-
检测缓冲区溢出。在分析、测试和生产构建中,对于 Clang 和 GCC 12.0 之前的版本,建议使用
-
-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,适用于新代码。
-
设置编译器警告级别为 4,大致相当于 GCC 和 Clang 的
-
/Wall:- 不建议使用,会产生大量误报。
-
/WX:-
将警告转换为错误,与 GCC 和 Clang 的
-Werror类似。
-
将警告转换为错误,与 GCC 和 Clang 的
-
/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 语言程序。
超级会员免费看

被折叠的 条评论
为什么被折叠?



