18.1 环境
1. 翻译环境与执行环境
C 语言将一个程序的处理和运行划分在两个不同的环境中:翻译环境(Translation Environment)与执行环境(Execution Environment)。
1.1 翻译环境
在翻译环境中,编译器把 C 源代码翻译为可执行程序,通常要经过预处理、编译、汇编和链接。翻译期间只能检查静态性质,例如语法是否正确、类型是否匹配、约束是否被违反;程序运行时的输入数据在这时无法得知。
1.2 执行环境
在执行环境中,翻译得到的程序在目标平台上运行。在宿主环境(有操作系统,如 Linux、Windows)中,程序由操作系统加载并运行;在独立环境(无操作系统,如单片机裸机程序、操作系统内核)中,程序在硬件复位后直接运行。
程序运行时才产生动态行为,例如与用户进行输入输出 (I/O) 交互、申请动态内存、调度多个线程。许多错误只有在程序运行、处理实际数据时才会暴露出来。
2. 宿主实现与独立实现
C 语言中,实现是一套特定的软件,它在特定的翻译环境下运行(并受特定控制选项影响),执行程序的翻译工作,并支持函数在特定的执行环境中执行。通常指编译器 + 标准库 + 目标运行平台。在宿主实现 (Hosted Implementation) 中,有操作系统支持(比如你现在学 C 语言使用的编译器,还有你电脑上搭载的操作系统);在独立实现 (Freestanding Implementation) 中,则没有。
2.1 宿主实现
只要你写的是一个严格符合 C 标准的程序,宿主实现就必须无条件接受并成功运行它。此时,你可以畅通无阻地使用所有的 C 标准库,比如写 printf、用 malloc。
PS:我相信几乎所有读者使用的都是宿主实现。
2.2 独立实现
由于独立环境没有操作系统支持,它无法提供像 printf(需要文件系统和屏幕驱动)或 malloc(需要复杂的系统内存管理)这样的高级功能。
但只要你写的程序足够简单(只依赖标准库当中的一小部分),那么独立实现也得保证必须能够成功编译并运行它。
可通过以下方式判断使用的是何种实现:宿主实现输出 1,独立实现输出 0。
#include <stdio.h>
int main(void) {
#ifdef __STDC_HOSTED__
printf("__STDC_HOSTED__ = %d\n", __STDC_HOSTED__);
#endif
return 0;
}2
3
4
5
6
7
8
可能的输出(示例):
STDC_HOSTED = 1
3. 习题
3.1 巩固
下面五段程序各自在不同环节出问题:
/* a.c */
int main(void) {
int x = "abc";
return x;
}2
3
4
5
/* b.c */
int main(void) {
int x;
int y = x + 1;
return y;
}2
3
4
5
6
/* c.c */
#include <stdio.h>
int main(void) {
int d = 0;
if (scanf("%d", &d) != 1) {
return 1;
}
printf("%d\n", 10 / d);
return 0;
}2
3
4
5
6
7
8
9
10
11
/* d.c */
extern int helper(void);
int main(void) {
return helper();
}2
3
4
5
6
/* e.c */
#include <nosuch.h>
int main(void) {
return 0;
}2
3
4
5
6
要求:
- 用
gcc -std=c17 -Wall -Wextra逐个编译,能链接的就链接,把每一段的诊断或结果记下来。 - 对每一段指出问题最早在哪一步被发现:预处理、编译、汇编、链接,还是运行。前四步属于翻译环境,最后一步属于执行环境。
c.c用输入0运行一次,记录进程的结局。它在编译期有诊断吗?这说明翻译环境能检查什么、不能检查什么?a.c与b.c的结局和编译器版本有关:GCC 13.2 上两者都只给警告,程序照常生成并运行;GCC 14.2 上a.c的-Wint-conversion已经算错误。分别说明运行时得到的值为什么不能当作正确结果,并据此解释「编译通过」为什么不能当成正确性的证据。
验收:五段各有一条记录,分类正确;c.c 在输入为 0 时异常终止,而不是打印一个商;能指出 d.c 的 undefined reference 是链接阶段才出现的。
正文用 __STDC_HOSTED__ 判断宿主实现与独立实现。这个宏由编译选项决定,不会跟着你实际链接的库走。
要求:
- 写一个程序打印
__STDC_HOSTED__的值,分别用下面三条命令编译,记录结果:
gcc -std=c17 -Wall -Wextra env.c -o env
gcc -std=c17 -ffreestanding -Wall -Wextra env.c -o env_free
gcc -std=c17 -ffreestanding -Wall -Wextra -nostdlib env.c -o env_bare2
3
- 第二条命令生成的程序照样能调用
printf,也能正常输出。用gcc -std=c17 -ffreestanding -c env.c编出目标文件,再用nm -u看它引用了哪些符号,解释-ffreestanding到底改变了什么。 - 第三条命令在链接阶段失败。抄下报错,指出缺的是启动代码还是标准库函数。
- 用一句话说明:
__STDC_HOSTED__描述的是实现的承诺,还是这台机器上装了哪些库。
验收:三条命令的结果都记录在案;能说清「宏变成 0」与「真的没有标准库可用」是两件事。
3.2 编程
写一个日志模块 log_sink.c,同一份源码在宿主实现和独立实现下都能编译通过:
/* log_sink.h */
void log_line(const char *text);
#if !__STDC_HOSTED__
typedef void (*log_sink_fn)(const char *text);
void log_set_sink(log_sink_fn fn);
#endif2
3
4
5
6
7
要求:
- 宿主实现下,
log_line用fputs与fputc把文本和换行写到stderr,只包含<stdio.h>。 - 独立实现下,模块内部保存一个函数指针,由
log_set_sink设置;log_line调用它,指针为空时什么也不做。这一分支不引入任何标准输入输出符号。 - 两种实现用
#if __STDC_HOSTED__分成两个分支,log_line的入口只写一次。 - 分别编译成目标文件,比较未定义符号:
gcc -std=c17 -Wall -Wextra -c log_sink.c -o log_hosted.o
gcc -std=c17 -ffreestanding -Wall -Wextra -c log_sink.c -o log_free.o
nm -u log_hosted.o
nm -u log_free.o2
3
4
- 写一个测试
main.c,用-ffreestanding编译,和log_free.o链接(链接时照常使用宿主工具链的 C 库),注册一个用printf输出[sink] 文本的函数,确认输出一行。
验收:nm -u log_hosted.o 里能看到 fputs 与 fputc;nm -u log_free.o 没有任何输出;测试程序输出一行 [sink] hello from freestanding object。若你的工具链在对象里额外带出 __main 一类启动符号,把它排除在比较之外,它来自编译器而不是模块。
3.3 综合与挑战
下面两个文件可以分别编译,链接也不报错,运行却会崩溃:
/* name.c */
int name(void) {
return 42;
}2
3
4
/* main.c */
#include <stdio.h>
char *name(void);
int main(void) {
printf("[%s]\n", name());
return 0;
}2
3
4
5
6
7
8
9
要求:
- 复现:分别编译、链接、运行,记录结局;用
nm name.o与nm main.o看name在两边的符号类型(T还是U)。 - 解释翻译环境为什么发现不了:编译器一次处理几个翻译单元,能看到什么、看不到什么?链接器又只核对什么?
- 修正:把声明放进公共头文件
shared.h,两个源文件都包含它,确认编译、运行正确。再把name.c的返回类型故意改成int编译一次,记录编译器现在给出的诊断。 - 在修正前的两个文件上加
-flto -Werror再编译一次,抄下-Wlto-type-mismatch的诊断。说明 LTO 为什么能看到跨翻译单元的类型,以及为什么不能指望每个项目都开 LTO。
验收:修正后输出一行方括号里带 mdr 的文本;-flto -Werror 下编译失败,诊断同时点出两个文件的声明位置。