系统编程基本功 — 库、系统调用、错误处理、命令行
系统编程基本功 — 库、系统调用、错误处理、命令行
这一章讲什么: 上一章搭了概念的地基,这一章教手艺——之后每一章都要用的 那批「通用动作」:看懂程序链接了哪些库;搞清一次系统调用从 C 代码到内核再 回来到底走了哪几步;出错时errno 那套约定怎么查、怎么报;怎么写程序才能在 别的 Unix 上编译得动;以及怎么解析命令行参数、读环境变量、把字符串变数字。 这是全书「工具箱」的一章,后面的项目章默认你会用这里的每一件工具。
1. 顶层全景:程序向内核要服务,一共有四条路
你的程序
│
├─① 调一个不需要内核的库函数 ────────────────┐ 纯用户空间,从不进内核
├─② 调一个库函数,库内部再替你调系统调用 ──────┤
├─③ 直接调系统调用的包装函数(如 getpid()) ────┼─► 进内核,回来带回执
└─④ 罕见:用 syscall() 亲手按号码拨号 ────────┘
│
▼
trap 指令(模式切换)
│
▼
内核的系统调用处理程序 → 查表 → 内核服务代码图说:①②③④是原书总结的四条路径1。本章的核心原理分五块: 库(代码从哪来)→ 系统调用的机制(路怎么走)→ 错误(带回执出了问题怎么办) → 可移植性(换台机器还能不能走)→ 命令行与环境(用户怎么给你下指令)。
主线例子全章共用:一个故意把缓冲区写得太小的 gethostname 程序——它每次
调用必失败,正好把「系统调用怎么报告错误」这条线从头到尾照亮。
2. 库:别人编译好的代码,怎么装进你的程序
为什么要库
你写的几乎每个程序都在调你没写过的函数。这些函数按「目标库」(object library)组织:一个把多个函数编译好的目标代码打包起来的文件,程序可以 方便地调用它2。库的动机很实际:与其把常用函数在多个项目里复制来复制去 (改一个 bug 要改 N 处),不如打包成库,谁用谁链3。
一个库文件里有索引(index)——一张「符号 → 文件内地址」的查找表, 让链接器能快速找到某个函数在哪个目标模块里4。
静态库与共享库:链接的时机不同
两类库的分水岭只有一条:代码在什么时刻并进你的程序。
静态库(static library,名字以 .a 结尾——archive 的缩写):
链接器在构建可执行文件时,把你用到的函数代码从库里复制出来,
插进可执行文件,然后解析所有未定义符号5。特点是可执行文件自包含:
拷到别的同架构机器上,即使没有那些库文件也能跑——这是静态链接如今
几乎唯一的理由(比如程序依赖某个特定版本的图形库,而目标机器上不一定有)6。
共享库(shared library,.so 结尾,shared object 的缩写):
代码不复制进可执行文件,而是到程序运行时才链接——所以也叫动态库
(dynamically linked library)。注意别和 Windows 的 DLL 混为一谈:名字
像,机制不同,原书特意只用 shared library 这个词7。文件名里的数字是版本号
(如 libc.so.6)8。
动态链接的具体过程:链接器往可执行文件里插入「这些符号运行时才解析」的记录;
程序加载时,动态链接器检查库是否已在内存,没有就找地方加载;
程序跑到某个未解析符号,动态链接器当场把它连上——所以有一点点运行时开销9。
Linux 有两个动态链接器:ld.so(老式 a.out 格式)和 ld-linux.so
(现代 ELF 格式);ELF(Executable and Linking Format)是如今几乎所有
Unix 厂商采用的格式,取代了更老的 a.out 和 COFF10。
共享库的四个优点
原书列得很全,浓缩成一张表11:
| 优点 | 机理 |
|---|---|
| 可执行文件更小 | 库代码不在文件里,加载更快、占磁盘更少 |
| 内存用得省 | 一份库在内存,所有进程共享链接到它(库的代码设计成运行时不可修改) |
| 库升级不用重编译 | 只要接口没变,换库文件即可;静态链接则所有程序都要重链 |
| 更安全 | 静态链接时代码地址固定,只有栈和堆能随机化;动态链接时内核可以把库加载到任意地址、每次运行都不一样,大幅加难针对固定地址的攻击 |
查看库和可执行文件的工具箱
原书给了一整套查看工具,这是「摸清楚一个二进制文件里有什么」的家当12:
| 工具 | 干什么 | 例子 |
|---|---|---|
ar t | 列静态库的目标模块表(注意 t 是操作码,前面不加 -) | ar t libstdc++.a |
objdump -a | 看归档索引;-t 只看静态符号 | objdump -a libsnw.a |
nm -D | 看共享库的动态符号;-j 只打印符号名 | nm -j -D libc.so.6 |
readelf -s | 显示任何 ELF 文件的符号表 | readelf -s myprogram |
hexdump / od | 以字符/十六进制/八进制逐字节转储文件 | 看二进制文件原始字节 |
ldd | 列出一个可执行文件链接了哪些共享库 | ldd /bin/ls |
ltrace / strace | 看程序运行时实际调了哪些函数/系统调用 | 见各自 man 页 |
readelf -s 输出里值得认一眼的一行:未绑定的符号值是 0——因为真正的地址绑定
要等到运行时13。ldd hello 的典型输出里除了 C 库 libc.so.6 和动态链接器
ld-linux.so-64.so.2,还有个 linux-vdso.so.1:那是 C 库运行时用来优化
性能的内置库,man 页第 7 节的 vdso 有解释14。
glibc:Linux 上的那份 C 标准库
GNU/Linux 上你用的 C 标准库实现是 glibc(GNU C Library);无论在哪个
Unix 上,这份库的文件名约定都叫 libc.so15。它甚至可以直接运行来报版本:
$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.39-0ubuntu3.1) stable release version 2.39.图说:一个共享库文件长着可执行文件的样子,是因为它的加载方式类似。
程序里可以用 gnu_get_libc_version() 函数拿到版本字符串,按版本号
决定程序的行为分支16。
3. 系统调用:一次从 C 代码跳进内核的完整行程
和普通函数调用差在哪
普通函数调用:跳进一个已经链接进你程序的例程,执行,返回——全程用户模式。
系统调用:看起来一样(跳入、返回),但跳进的是内核里的函数,而且途中
发生一次处理器模式的切换17。怎么分辨一个函数是不是系统调用?两个土办法:
man 页在第 2 节的大概率是系统调用(库函数在第 3 节);SYNOPSIS 里要求
include <unistd.h> 的,多半是系统调用18。
trap:模式切换的那道门
切换靠一条机器指令——trap(陷阱指令):它改变处理器模式,并跳到内存中
一个固定位置19。老系统用 int 0x80;Linux 2.6 以后的内核用 sysenter
指令,glibc 2.3.2 起也跟进20。记住这个词:系统调用的本质就是「执行一条
trap」,其余全是配套。
系统调用号与参数传递
内核给每个系统调用编了号,作为系统调用向量表(一张地址表)的下标;
trap 指令带着这个号,内核的处理程序一查表就知道该跑哪段内核代码。
号随系统而异,但有符号宏名——如 getpid() 的号叫 __NR_getpid
(兼容旧名 SYS_getpid);截至原书写作,最新 Linux 内核约有 450 个
系统调用21。syscalls man 页列出本系统支持的全部名单。
参数怎么传给内核?两种办法,按参数个数选22:
- 寄存器法:参数按约定顺序放进约定的寄存器;寄存器不够用时……
- 块法:参数存进内存里一块连续字节,块的地址放进一个寄存器。
Linux 目前不允许一个系统调用超过 6 个参数23。
包装函数:你其实从没直接调过系统调用
这是本章最反直觉、也最重要的一句:进程通常不直接发起系统调用,而是调 包装函数(wrapper function)24。glibc 为几乎所有系统调用提供了包装, 名字往往和系统调用一样。包装干的事:把参数从栈上搬到内核要的寄存器里、 执行 trap、拿回返回值、设置全局错误变量——薄得像一层膜(thin)25。
也有不薄的例子:truncate() 系统调用原先用 32 位长度;文件系统支持大文件后,
内核加了 64 位版 truncate64()。glibc 的 truncate() 包装函数会在运行时
判断内核提供哪个版本,替你调对的那一个26。所以「库函数 vs 系统调用」
的边界比想象中糊——你调的 truncate 是库函数,它里面选路。
少数系统调用没有包装(如旧版本 glibc 里的 gettid()),这时只能用
syscall() 函数:把系统调用号和参数直接递进去27:
#include <sys/syscall.h>
printf("Thread id %ld\n", syscall(SYS_gettid)); /* glibc 2.30 起才有 gettid() 包装 */六步走完一次系统调用
原书把全过程总结成六步,照抄这个顺序(这就是 §1 那张图的内核半边的细节)28:
1. 用户程序像调普通函数一样调库里的包装函数
2. 包装函数把参数从栈上搬进内核约定的寄存器
3. 包装执行 trap,把系统调用号作为参数传进去
→ 处理器切到 supervisor 模式,跳进内核的系统调用处理程序
4. 处理程序拿号查系统调用向量表 → 得到该调用的内核代码地址
5. 真正的内核服务代码执行,把返回值交回处理程序
6. 处理程序把返回值递给包装函数;返回指令切回用户模式
→ 若出错,包装函数把错误码交给程序(errno 登场,下一节)图说:三步在用户模式(1、2、6 的后半),三步在内核模式(4、5、6 的前半), trap 是唯一的门。 把这张图和第 1 章 §19 的「门铃」图叠起来看, 「系统调用 = 门铃 + 服务窗口 + 回执」就有了机械结构。
判断(我们的,不是书里的): 「包装函数很薄」这句话是理解 glibc 的钥匙, 但别把它当成永远成立——
truncate的例子说明,库会在薄包装里偷偷做 「版本适配」这种重活。所以排查「同样一个调用在两台机器上行为不同」时, 值得怀疑的第一层不是内核,而是两台机器的 glibc 版本不同,包装替你 选的路不同。查法上一节就给过:gnu_get_libc_version()。 如果错,会错在: 如果两台机器 glibc 同版本而行为仍不同,那问题多半真的 在内核 side(系统调用实现差异),这层怀疑就该往后放。
4. 错误处理:errno、perror 与 strerror
系统调用怎么报告错误
规矩:几乎所有系统调用出错时返回负数,绝对值就是错误类型29。
trap 回来之后,如果返回值是负的,包装函数做两件事:把绝对值存进一个
叫 errno 的静态变量(定义在 <errno.h>),然后给程序返回 -130。
程序 include errno.h 就能读到 errno。
两条纪律,原书用边框强调31:
- 程序自己不许声明 errno——
errno.h里已经有了;你再声明一个会把它盖住, 你读到的就不是真错误值。 - errno 必须紧跟着那次调用就读——中间再调任何函数或系统调用, 错误值可能被覆盖。
errno -l 命令能列出全部错误码(属于 moreutils 包,可能要自己装):
$ errno -l
EPERM 1 Operation not permitted
ENOENT 2 No such file or directory
ESRCH 3 No such process
EINTR 4 Interrupted system call图说:错误码是「名字 + 号 + 一句话」三列。程序里用符号名(EPERM、ENOENT), 别背数字。 man 页每个函数的 ERRORS 节列的就是它可能返回哪些符号名32。
主线例子:gethostname 必败演示
现在走主线例子。系统调用 gethostname() 把本机主机名存进你给的数组,
原型(注意第二个参数的类型——size_t,POSIX 定义的「存对象大小」专用
无符号整数类型,这是全书第一个 Unix 系统类型)33:
#include <unistd.h>
int gethostname(char *name, size_t len);man 页说它可能返回三个错误:EFAULT(name 是无效地址)、EINVAL(len 是
负数)、ENAMETOOLONG(len 比主机名实际长度小)34。还有一个坑:名字太长
装不下时会截断而不是报错,且 POSIX 不保证截断后还有结尾空字符——直接
printf("%s") 可能打出越界内容,最常见就是段错误35。
于是原书写了 gethostname_demo.c,故意把数组开成 4 字节、len 给 3,
让错误必然发生:
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
void main()
{
char name[4]; /* 故意小到装不下任何主机名 */
size_t len = 3;
int returnvalue;
returnvalue = gethostname(name, len); /* 发起调用 */
if ( -1 == returnvalue ) { /* 出错分支 */
switch ( errno ) {
case EFAULT:
printf("A bad address was passed for the string name\n"); break;
case EINVAL:
printf("The length argument was negative.\n"); break;
case ENAMETOOLONG:
printf("The hostname is too long for the allocated array.\n");
}
}
else
printf("%s\n", name);
}假设主机名是 harpo,运行输出36:
$ ./gethostname_demo
The hostname is too long for the allocated array.图说:每一步都有具体状态——name[4] 是起点,len=3 是触发器,
返回 -1 是回执,errno==ENAMETOOLONG 是细节,case 分支是处置。
这就是「检查返回值 + 读 errno + 分支处理」三件套的标准写法。
perror 与 strerror:报错的省力写法
手写 switch 太啰嗦时,两个库函数能直接生成错误消息(都声明在各自的头文件里, 而且都locale 感知——用户的 locale 不是英语时消息会被翻译,只要系统支持; locale 第 3 章细讲)37:
perror(s):把字符串 s 接上一条对应 errno 的预定义消息,写到标准错误流。 惯例把出错的函数名传进去:perror("gethostname")输出gethostname: File name too long38。缺点:拿不到分支处理的机会 (不能对不同错误做不同动作),消息也不能定制。strerror(errnum):返回错误号对应的消息字符串,用法printf("gethostname failed: %s\n", strerror(errno))39。
用 errno 的整条替代链,从繁到简:switch(errno)(可定制)→ perror(最省事) → strerror(折中)。三个都得紧跟调用之后用。
库函数的错误约定:四类,各不相同
别以为所有调用都和系统调用一个规矩。原书把库函数的错误行为分成四类40:
| 类别 | 行为 | 例子 |
|---|---|---|
| ① 和系统调用一样 | 返回 -1 并设 errno | 大部分 |
| ② 不返回 -1,但设 errno | 用自己的方式报失败 | malloc() 失败返回 NULL 指针,errno=ENOMEM(内存不够) |
| ③ 不用 errno | 用专属返回值 | fgetc()/getc() 出错返回 EOF,errno 不设 |
| ④ Pthreads 约定 | 成功返回 0,失败返回正数错误码 | 第 15 章的线程函数全家 |
图说:没有统一规矩,只有统一答案——查 man 页。 你用的那个函数属于哪类, 唯一权威是它自己的 man 页41。第 15 章线程函数的「错误码直接当返回值」 约定,到时候会专门提醒。
5. 可移植性:一套代码,多种 Unix
问题从哪来
可移植性(portability)= 程序能在别的机器上不改或微改就运行的程度。 Unix 的混乱生长造成了三大支系(BSD、GNU/Linux、System V),同名函数 在两支上语义可能不同42。给源码发行、让用户自己编译,是最理想也最脆弱的 场景——你控制不了对方的系统。
特性测试宏:编译期的开关
解法藏在 C 预处理器的条件编译里,开关就是特性测试宏(feature test
macro)。头文件里到处是这种结构(这是 stdio.h 里的真实代码,简化过)43:
#ifdef __USE_GNU
extern int fcloseall (void);
#endif——fcloseall() 的声明只在 __USE_GNU 有定义时才暴露给编译器。man 页
SYNOPSIS 里的「Feature Test Macro Requirements」一节就是在告诉你:用这个
函数之前得先定义哪个宏、在哪个 include 之前44。比如 getline():
getline(), getdelim():
Since glibc 2.10: _POSIX_C_SOURCE >= 200809L
Before glibc 2.10: _GNU_SOURCE翻译:glibc 2.10 起定义 _POSIX_C_SOURCE >= 200809L 即可;更老的 glibc
得用 _GNU_SOURCE45。定义方式两种:源码里 #define _GNU_SOURCE
(必须出现在所有 include 之前),或编译命令行 gcc -D_GNU_SOURCE ...46。
宏还有组合逻辑:_DEFAULT_SOURCE || (_XOPEN_SOURCE && _XOPEN_SOURCE < 500)
表示「或者定义前者,或者定义后者且值小于 500」47。常见的几张脸48:
| 宏 | 暴露什么 |
|---|---|
_POSIX_SOURCE | POSIX.1 功能 + 全部 ISO C 特性 |
_POSIX_C_SOURCE | 由数值控制开放哪版 POSIX 功能 |
_XOPEN_SOURCE | POSIX.1、POSIX.2 和 X/Open 的功能 |
_GNU_SOURCE | glibc 的一切:C89/C99、POSIX.1/2、BSD、SVID、X/Open、LFS 和全部 GNU 扩展(POSIX 与 BSD 冲突时 POSIX 优先) |
_BSD_SOURCE | 4.3 BSD 系功能 + ISO C + POSIX.1/2 |
_SVID_SOURCE | System V 系(SVID)+ ISO C + POSIX + X/Open |
一句话记法:_GNU_SOURCE 是把所有闸门都拉开;想要可移植就别拉它,
用 _POSIX_C_SOURCE precise 地要你要的标准。要深入就读
feature_test_macros(7) 这一页 man49。
回到第 1 章的伏笔: gettid_demo.c 里的 #define _GNU_SOURCE 现在就
讲透了——gettid() 是 Linux 私有功能,不推这扇门,头文件不给你看声明。
系统限制:有些数,运行时才查得到
Unix 给资源设上限:文件名最长多少、路径最长多少、用户名最长多少……
标准规定的是这些上限的最小值(如 POSIX 规定的 _POSIX_NAME_MAX),
叫系统限制(system limits)50。可移植程序不能把这些数写死,得运行时查。
POSIX 把系统限制分三类51:
| 类别 | 含义 | 例子 |
|---|---|---|
| 运行时不变 | 任何系统上都是常数 | 大多在 <limits.h> 里 |
| 路径名可变 | 随文件系统而变,同一台机器不同盘可能不同 | 文件名最长 |
| 运行时可增 | 值可以在运行时调高 | 某些资源上限 |
查询工具是 pathconf()(查和路径相关的)和 sysconf()(查其余)两个函数,
后面的章节会有现成例子52。
国际化(i18n):程序要会说话
早期软件都为英语用户写;现在系统遍布全球,文化约定不同会直接造成歧义—— 原书给的两个例子足以警醒53:
- 日期:美国 MM/DD/YYYY,欧洲 DD/MM/YYYY。
07/11/2033是 11 月 7 日 还是 7 月 11 日? - 数字:美国千分位用逗号、小数点用点(
1,048,576.00);欧洲反过来 (1.048.576,00)。只认一种写法的程序换个地方就崩。
locale 就是把这些文化差异收拢成一个概念:POSIX 的定义是「用户环境中 依赖语言和文化习俗的那部分子集」54。一个程序无论在哪儿跑都行为正确、 输入输出和当地一致,就叫它被国际化(internationalized,i18n)了。 第 3 章专门展开——本章只需要你记住:locale 不是锦上添花,是正确性问题。
6. 命令行与环境:程序出生的那一刻拿到什么
argc 与 argv:shell 替你切好的词
敲 $ gcc -o main main.c utils.c 回车时,shell 负责把行拆成词、找出
重定向、可能还要做变量展开,然后把这串词以一个 NULL 结尾的字符串数组
交给程序的 main()55。C 标准要求 main 可以这样声明:
int main (int argc, char *argv[])argc:命令行上的词数(含程序名自己,所以 argc ≥ 1);argv:词数组,argv[0]永远是程序名,参数在argv[1]…argv[argc-1], 最后一个元素之后是 NULL 指针56。
图说:数组的边界有两个看门人——argc 报数,NULL 收尾。
引号能改变切词:showargs todo-list : 'debug prog.c' email 是 5 个词,
单引号把带空格的 debug prog.c 粘成了一个词57。遍历有两种写法:
for (i = 1; i < argc; i++)(数数)或 while (argv[i] != NULL)(找 NULL),
两者等价58。参数名 argc/argv 只是约定,不是关键字——但谁都看得懂的代码
别起怪名。
读环境变量:三条路
程序继承的环境(第 1 章 §7)有三种访问法59:
getenv("名字")——首选。找到就返回值字符串的指针,找不到返回 NULL。 两个坑:① 返回的是环境列表内部的指针,不是拷贝,别拿它当数组改; ② POSIX 允许实现把返回值存在静态存储区,再调一次getenv可能把它 覆盖——要留着用就先strncpy拷走60。- 全局变量
environ(extern char **environ;)——指向整个环境列表起点, 顺序扫描可遍历全部(等于自己写一遍printenv)。只为找一两个变量就别用它: 列表无序,最坏情况要逐条比对所有变量名,而getenv内部做得更高效61。 main的第三参数envp[]——和 environ 等价,但 POSIX.1-2024 不认, 有的系统上根本不能用。原书建议:别用62。
用法错误:程序的第一道防线
等参数的程序必须先查 argc,否则访问不存在的数组位置就是致命错误。标准姿势 (这也成了全书所有程序的模板)63:
if ( argc < 3 ) {
fprintf(stderr, "usage: %s file1 file2\n", argv[0]);
exit(1);
}两个细节:报错走 stderr(标准错误流,第 4 章细讲)而不是 stdout——
错误消息不该混进正常输出;用法串里带 argv[0],用户看到的是自己敲的名字。
注意 ~/bin/myprog infile 运行时 argv[0] 是整个路径
/home/stewart/bin/myprog(shell 先把 ~ 展开成了家目录路径)64。
程序名:剥掉路径的两种剥法
只想显示 myprog 不要前面一长串?两种办法65:
strrchr(argv[0], '/'):返回字符串里最后一次出现/的位置; 找到了就打印它后面那段,找不到说明本来没有目录部分。顺带一个 C 语言点: 指针算术里p + 1加的是一个元素的宽度不是一字节——double指针 加 6 是地址加 4866。basename():更省事,但有两个版本——POSIX 版声明在<libgen.h>(会修改入参,glibc 实现还有 bug),GNU 版在<string.h>(不改入参)。 用 GNU 版要_GNU_SOURCE67。
getopt:别自己解析选项
选项组合千奇百怪(ssh -acCfGgKkMN 这样叠写、参数跟在后面、GNU 还允许
选项参数可选、参数在选项前……),自己解析又臭又易错。Unix 提供了
getopt()(短选项)和 getopt_long()(长短都行,GNU 扩展,要
_GNU_SOURCE)68。要用它们得 include <unistd.h>——虽是库函数,
规矩照系统调用;四个外部变量 optarg、optind、opterr、optopt
库已定义,程序不许自己声明69。
核心在选项字符串的语法70:
| 写法 | 含义 |
|---|---|
h | -h 是无参开关 |
b: | -b 有必需参数,值放进 optarg;缺了返回 ? |
b:: | -b 有可选参数,给了放进 optarg,没给置 0(GNU 扩展) |
(开头): | 选项串以 : 开头时,缺必需参数返回 : 而不是 ?——这样能把「无效选项」和「缺参数」两种错误分开 |
工作方式:反复调 getopt(argc, argv, optstring),每次返回一个选项字符;
返回 -1 表示没有更多选项,此时 optind 是 argv 里第一个非选项词的
下标(GNU 版会顺手把 argv 重排,让所有非选项挪到末尾)71。
图说:一次 getopt 循环 = 收割机走过一遍麦田;返回的每个字母是一束麦穗,
optarg 是穗上的谷粒,optind 最后指向没割的那行。 程序结构因此定型为
两段式:先用循环 + switch 把所有选项收进变量,再按这些变量决定程序
行为72。switch 里每个 case 只做记录(如 opt_h = TRUE),决策一律
放到循环外——这是原书示范的关键纪律,别把业务逻辑塞进解析循环。
字符串转数字:用 strtol,别用 atoi
命令行参数和环境值都是字符串,哪怕是数字。转换函数两大家族73:
| 家族 | 错误检查 | 进制 |
|---|---|---|
atoi / atol / atof | 无——出错返回 0,和「真值是 0」分不清 | 只认十进制 |
strto*(strtol、strtoll、strtoul、strtod…) | 出错置 errno,能查 | 2 到 36 任意进制 |
atoi 系的 man 页自己都劝退74;作者坦白:写一次性脚本时他偶尔也用,
但正经代码请用 strtol75。
strtol(字符串, &endptr, base) 三参数,配上 errno 是四查法(原书
strtol_demo.c 的完整流程)76:
errno = 0; /* ① 先清零:真值可能是 0,分不清 */
val = strtol(argv[1], &endptr, 0); /* base=0:自动识别 0x 十六进制、0 开头八进制 */
if ( errno != 0 ) { perror("strtol"); exit(EXIT_FAILURE); } /* ② 转换错误(如超范围) */
if ( endptr == argv[1] ) { /* ③ 第一个字符就无效:根本不是数字 */ }
if ( *endptr != '\0' ) { /* ④ 数字后面拖着别的字符,如 "1234abc" */ }
printf("strtol() returned %ld\n", val);四种输入各就各位(都是原书真实运行记录)77:
$ ./strtol_demo 100000000000000 → strtol() returned 100000000000000
$ ./strtol_demo +871237abns → Characters following the number: "abns" 再返回 871237
$ ./strtol_demo kjasdksd → No digits were found
$ ./strtol_demo 71238172381273687236817226 → strtol: Numerical result out of range
$ ./strtol_demo 032 → strtol() returned 26 ← 0 开头被当八进制!最后一行是著名的坑:base 传 0 时,032 被当八进制,等于十进制 26。
sscanf(str, "%d", &x) 也能转,但大批量转换慢、错误处理也不如 strtol 健壮78。
7. 作者的判断与证据
有明证的:
- 静态链接「如今几乎唯一的理由」是可执行文件自包含、跨机可靠运行6——这是 对行业现状的观察,可检验。
- 动态链接让库代码地址每次运行都可随机化,使针对固定地址的攻击难得多11—— 安全事实,与 ASLR 机制的公开资料一致。
- glibc 为几乎全部系统调用提供包装、
gettid在 glibc 2.30 前无包装2427 ——可核验的实现细节。 - errno 两条纪律(不自己声明、立即读)是 POSIX 语义的一部分,不是风格建议31。
作者的判断(经验之谈):
- 「写库升级不用重编译的前提是接口没变」11——实践中接口「变而不破」 (soname 策略)才是完整答案,原书这里简化了。
- 「一次性代码可以用 atoi」75——个人工程口味,合理但与「永远检查错误」 的自家有张力。
- 「main 第三参数 envp 别用」62——建议是 POSIX 符合性的自然推论,立场稳。
8. 边界与局限
- 共享库的版本机制(soname、符号版本)本章没展开——只讲了「.so 后面的数字 是版本」。生产环境里库版本冲突是真实痛点,要补得读 Program Library HOWTO (原书在附录 A 里指了路)。
- strace/ltrace 只提了名字12,没教用法——这是调试系统程序的神器, 想深入要自己读 man 页。
- 系统限制一节只给了分类和
pathconf/sysconf的名字,例子散在后续章节52。 - locale 只给了日期和数字两个例子和定义,完整机制(第 3 章)才是正题54。
- getopt 覆盖了短选项与 GNU 长选项,但子命令式命令行(像 git 那样的
git <command>)没有现成库可用,那是另一个量级的解析问题。
9. 可带走的
主线例子,一行复述: name[4] + len=3 调 gethostname → 返回 -1 →
errno==ENAMETOOLONG → switch 打出「The hostname is too long…」;
换成 perror("gethostname") 一行解决,代价是失去分错误处置的能力。
- 查库内容:
ar t(静态)、nm -D(共享)、readelf -s(一切 ELF)、ldd(链接了谁)。 - 系统调用的机械结构 = 包装函数搬参数 → trap → 查向量表 → 内核代码 → 带回执;glibc 包装经常薄,但不总是(thuncate 会替你选版本)。
- 错误三件套:查返回值(-1 即错)、立刻读 errno、用 switch/perror/strerror 报;errno 不许自己声明,读完不许再调别的函数。
- 库函数有四类错误约定,没有通用答案,只能查 man 页(malloc 返 NULL、 fgetc 返 EOF、线程函数返错误码)。
- 要可移植,用
_POSIX_C_SOURCE精确开窗;要 Linux 全家桶,才推_GNU_SOURCE——推门动作本身就是「我知道这会牺牲可移植性」的签名。 - 系统限制分三类,别写死数字,用
pathconf()/sysconf()运行时查。 - argc/argv 是 shell 切好的词,argv[0] 是程序名(可能是全路径), 数组以 NULL 收尾。
- 读环境首选
getenv;返回值是指针不是拷贝,要用先strncpy备份。 environ变量能遍历整个环境(等于自己写 printenv);main 的 envp 参数 POSIX 不认,别用。- 选项解析交给
getopt/getopt_long;程序结构 = 循环收割选项 → 循环外 按选项决策。 - 字符串转数字用
strtol四查法(errno 清零 → 查 errno → 查 endptr 起点 → 查尾巴);记住032是八进制。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 四条通往内核服务的路径 | Chapter 2: Fundamentals of System Programming | text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:524(搜「satisfied by」) · :535(搜「User space」) |
| 目标库、索引 | 同上 | :71(搜「object library」) · :89(搜「look-up table」) |
| 静态库与共享库定义 | 同上 | :123(搜「copies the library functions」) · :130(搜「runtime」) · :136(搜「Microsoft calls a DLL」) |
| 动态链接过程、ld.so、ELF | 同上 | :158(搜「resolved when」) · :167(搜「Linking Format」) |
| 共享库优点(含安全) | 同上 | :172(搜「doesn't contain the code」) · :186(搜「Methods of deterring」) |
| 查看库的工具、ldd、vdso | 同上 | :196(搜「ar command」) · :247(搜「nm command」) · :264(搜「readelf」) · :298(搜「ldd」) · :309(搜「linux-vdso」) |
| glibc 与版本查询 | 同上 | :336(搜「libc.so」) · :358(搜「gnu_get_libc_version」) |
| 系统调用与 trap | 同上 | :390(搜「significantly different」) · :438(搜「trap instruction」) · :409(搜「sysenter」) |
| 系统调用号、参数传递、6 参数上限 | 同上 | :418(搜「450」) · :424(搜「Register method」) · :429(搜「six parameters」) |
| 包装函数、truncate、syscall() | 同上 | :433(搜「wrapper functions」) · :439(搜「thin」) · :450(搜「decides which one」) · :499(搜「may not have a wrapper」) |
| 系统调用六步 | 同上 | :453(搜「steps that take place」) |
| errno 机制与两条纪律 | 同上 | :557(搜「negative number」) · :563(搜「static variable named errno」) · :575(搜「must not declare」) · :579(搜「inspect errno immediately」) |
| errno -l 与 gethostname 例子 | 同上 | :589(搜「errno -l」) · :612(搜「size_t」) · :630(搜「EFAULT」) · :678(搜「too long for the allocated array」) |
| perror、strerror、四类库错误约定 | 同上 | :682(搜「locale aware」) · :722(搜「File name too long」) · :735(搜「pointer to a string」) · :761(搜「four different categories」) |
| 可移植性与三大支系 | 同上 | :784(搜「haphazard growth」) |
| 特性测试宏 | 同上 | :805(搜「macro designed to expose」) · :842(搜「Feature Test Macro Requirements」) · :899(搜「-D option」) · :932(搜「logical-AND」) · :937(搜「_POSIX_SOURCE」) |
| 系统限制三分类 | 同上 | :968(搜「_POSIX_NAME_MAX」) · :976(搜「Runtime invariant」) |
| locale 与国际化 | 同上 | :993(搜「MM/DD」) · :1000(搜「commas」) · :1013(搜「subset of a user's」) |
| argc/argv 与切词 | 同上 | :1081(搜「NULL-terminated array」) · :1081(搜「argc ≥ 1」) · :1096(搜「showargs」) |
| getenv 三坑与 environ | 同上 | :1162(搜「getenv」) · :1185(搜「not to a copy」) · :1224(搜「externally defined」) · :1266(搜「printenv」) |
| main 第三参数 | 同上 | :1279(搜「envp」) · :1292(搜「doesn't support it」) |
| 用法检查、stderr、argv[0] 路径 | 同上 | :1322(搜「usage:」) · :1355(搜「contains the entire pathname」) |
| strrchr 与 basename 两版本 | 同上 | :1365(搜「strrchr」) · :1403(搜「pointer arithmetic」) · :1419(搜「libgen.h」) |
| getopt 与选项串语法 | 同上 | :1490(搜「getopt_long」) · :1507(搜「extern char」) · :1536(搜「A letter by itself」) · :1549(搜「leading character」) · :1558(搜「returns -1」) · :1560(搜「rearranges」) |
| 两段式解析结构 | 同上 | :1563(搜「two parts」) |
| strtol 与四查法、八进制坑 | 同上 | :1684(搜「No error checking」) · :1719(搜「long strtol」) · :1741(搜「errno = 0」) · :1799(搜「octal」) · :1809(搜「sscanf」) |
Footnotes
-
出处:「Chapter 2: Fundamentals of System Programming」第 523–530 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:524,搜「satisfied by」)。原文列了四条:不需要系统调用的库函数、需要系统调用的库函数、经包装函数的系统调用、罕见的 syscall() 直拨。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 71 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:71,搜「object library」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 46–70 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:54,搜「not a very efficient organizing」)。原文:复制复用的组织方式效率低——一个函数有 bug,得找到所有用到它的项目逐个改。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 89–91 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:89,搜「look-up table」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 119–128 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:123,搜「copies the library functions」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 151–156 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:152,搜「self-contained」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 135–138 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:136,搜「Microsoft calls a DLL」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 141–144 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:144,搜「shared object」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 157–164 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:158,搜「resolved when」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 165–169 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:167,搜「Linking Format」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 171–192 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:172,搜「doesn't contain the code」) · :177(搜「single memory-resident copy」) · :180(搜「don't need to be modified」) · :186(搜「Methods of deterring」)。 ↩ ↩2 ↩3 -
出处:「Chapter 2: Fundamentals of System Programming」第 194–329 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:196,搜「ar command」) · :247(搜「nm command」) · :264(搜「readelf」) · :291(搜「octal dump」) · :298(搜「ldd」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 283–285 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:283,搜「not yet bound」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 296–326 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:309,搜「linux-vdso」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 331–336 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:336,搜「libc.so」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 355–383 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:358,搜「gnu_get_libc_version」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 385–405 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:390,搜「significantly different」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 392–396 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:393,搜「Section 2」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 403–405 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:438,搜「trap instruction」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 407–410 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:409,搜「sysenter」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 411–420 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:418,搜「450」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 421–428 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:424,搜「Register method」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 429–430 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:429,搜「six parameters」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 432–436 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:433,搜「wrapper functions」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 439–441 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:439,搜「thin」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 442–450 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:450,搜「decides which one」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 490–521 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:499,搜「may not have a wrapper」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 452–471 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:453,搜「steps that take place」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 556–560 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:557,搜「negative number」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 561–565 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:563,搜「static variable named errno」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 573–581 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:575,搜「must not declare」) · :579(搜「inspect errno immediately」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 566–571 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:568,搜「ERRORS section」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 609–615 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:612,搜「size_t」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 628–632 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:630,搜「EFAULT」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 616–627 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:627,搜「segmentation fault」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 638–678 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:678,搜「too long for the allocated array」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 680–687 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:682,搜「locale aware」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 688–729 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:722,搜「File name too long」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 730–737 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:735,搜「pointer to a string」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 759–775 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:761,搜「four different categories」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 776–777 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:777,搜「read its man page」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 778–802 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:784,搜「haphazard growth」)。原文:over time, three major variants of Unix evolved: BSD, GNU/Linux, and System V。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 804–817 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:809,搜「#ifdef __USE_GNU」)。引用的 stdio.h 简化片段见第 809–812 段。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 842–856 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:842,搜「Feature Test Macro Requirements」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 883–896 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:884,搜「2.10」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 897–906 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:899,搜「-D option」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 918–934 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:932,搜「logical-AND」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 935–950 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:937,搜「_POSIX_SOURCE」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 951–953 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:951,搜「feature_test_macros」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 964–970 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:968,搜「_POSIX_NAME_MAX」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 971–980 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:974,搜「divides system limits」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 981–984 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:982,搜「pathconf」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 986–1010 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:993,搜「MM/DD」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1011–1021 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1013,搜「subset of a user's」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 1046–1068 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1058,搜「arranges for the list of words」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1074–1084 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1081,搜「argc ≥ 1」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1092–1112 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1096,搜「showargs」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1131–1147 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1133,搜「argv[i] == NULL」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1151–1155 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1153,搜「three different ways」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1184–1221 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1185,搜「not to a copy」) · :1197(搜「statically allocated」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1223–1274 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1266,搜「printenv」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1275–1294 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1292,搜「doesn't support it」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 1315–1338 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1322,搜「usage: %s file1 file2」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1349–1355 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1355,搜「contains the entire pathname」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1361–1364 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1363,搜「relatively portable ways」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1405–1409 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1407,搜「basetype」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1413–1425 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1419,搜「libgen.h」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1486–1493 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1490,搜「getopt_long」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1523–1525 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1507,搜「extern char」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1532–1552 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1536,搜「A letter by itself」) · :1549(搜「leading character」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1555–1561 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1558,搜「returns -1」) · :1560(搜「rearranges」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1562–1568 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1563,搜「two parts」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1682–1701 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1684,搜「No error checking」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1702–1710 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1703,搜「don't set the errno」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1708–1710 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1710,搜「throw-away code」)。 ↩ ↩2 -
出处:「Chapter 2: Fundamentals of System Programming」第 1716–1781 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1719,搜「long strtol」) · :1741(搜「errno = 0」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1782–1799 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1785,搜「100000000000000」) · :1799(搜「octal」)。 ↩ -
出处:「Chapter 2: Fundamentals of System Programming」第 1803–1839 段(
text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1836,搜「wouldn't recommend」)。 ↩