编程语言研究书库

系统编程基本功 — 库、系统调用、错误处理、命令行

系统编程基本功 — 库、系统调用、错误处理、命令行

这一章讲什么: 上一章搭了概念的地基,这一章教手艺——之后每一章都要用的 那批「通用动作」:看懂程序链接了哪些库;搞清一次系统调用从 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:

  1. 程序自己不许声明 errno——errno.h 里已经有了;你再声明一个会把它盖住, 你读到的就不是真错误值。
  2. 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_SOURCEPOSIX.1 功能 + 全部 ISO C 特性
_POSIX_C_SOURCE由数值控制开放哪版 POSIX 功能
_XOPEN_SOURCEPOSIX.1、POSIX.2 和 X/Open 的功能
_GNU_SOURCEglibc 的一切:C89/C99、POSIX.1/2、BSD、SVID、X/Open、LFS 和全部 GNU 扩展(POSIX 与 BSD 冲突时 POSIX 优先)
_BSD_SOURCE4.3 BSD 系功能 + ISO C + POSIX.1/2
_SVID_SOURCESystem 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:

  1. getenv("名字")——首选。找到就返回值字符串的指针,找不到返回 NULL。 两个坑:① 返回的是环境列表内部的指针,不是拷贝,别拿它当数组改; ② POSIX 允许实现把返回值存在静态存储区,再调一次 getenv 可能把它 覆盖——要留着用就先 strncpy 拷走60。
  2. 全局变量 environ(extern char **environ;)——指向整个环境列表起点, 顺序扫描可遍历全部(等于自己写一遍 printenv)。只为找一两个变量就别用它: 列表无序,最坏情况要逐条比对所有变量名,而 getenv 内部做得更高效61。
  3. 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") 一行解决,代价是失去分错误处置的能力。

  1. 查库内容:ar t(静态)、nm -D(共享)、readelf -s(一切 ELF)、 ldd(链接了谁)。
  2. 系统调用的机械结构 = 包装函数搬参数 → trap → 查向量表 → 内核代码 → 带回执;glibc 包装经常薄,但不总是(thuncate 会替你选版本)。
  3. 错误三件套:查返回值(-1 即错)、立刻读 errno、用 switch/perror/strerror 报;errno 不许自己声明,读完不许再调别的函数。
  4. 库函数有四类错误约定,没有通用答案,只能查 man 页(malloc 返 NULL、 fgetc 返 EOF、线程函数返错误码)。
  5. 要可移植,用 _POSIX_C_SOURCE 精确开窗;要 Linux 全家桶,才推 _GNU_SOURCE——推门动作本身就是「我知道这会牺牲可移植性」的签名。
  6. 系统限制分三类,别写死数字,用 pathconf()/sysconf() 运行时查。
  7. argc/argv 是 shell 切好的词,argv[0] 是程序名(可能是全路径), 数组以 NULL 收尾。
  8. 读环境首选 getenv;返回值是指针不是拷贝,要用先 strncpy 备份。
  9. environ 变量能遍历整个环境(等于自己写 printenv);main 的 envp 参数 POSIX 不认,别用。
  10. 选项解析交给 getopt/getopt_long;程序结构 = 循环收割选项 → 循环外 按选项决策。
  11. 字符串转数字用 strtol 四查法(errno 清零 → 查 errno → 查 endptr 起点 → 查尾巴);记住 032 是八进制。

10. 原文地图

主题原书章原文位置
四条通往内核服务的路径Chapter 2: Fundamentals of System Programmingtext/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

  1. 出处:「Chapter 2: Fundamentals of System Programming」第 523–530 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:524,搜「satisfied by」)。原文列了四条:不需要系统调用的库函数、需要系统调用的库函数、经包装函数的系统调用、罕见的 syscall() 直拨。 ↩

  2. 出处:「Chapter 2: Fundamentals of System Programming」第 71 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:71,搜「object library」)。 ↩

  3. 出处:「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,得找到所有用到它的项目逐个改。 ↩

  4. 出处:「Chapter 2: Fundamentals of System Programming」第 89–91 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:89,搜「look-up table」)。 ↩

  5. 出处:「Chapter 2: Fundamentals of System Programming」第 119–128 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:123,搜「copies the library functions」)。 ↩

  6. 出处:「Chapter 2: Fundamentals of System Programming」第 151–156 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:152,搜「self-contained」)。 ↩ ↩2

  7. 出处:「Chapter 2: Fundamentals of System Programming」第 135–138 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:136,搜「Microsoft calls a DLL」)。 ↩

  8. 出处:「Chapter 2: Fundamentals of System Programming」第 141–144 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:144,搜「shared object」)。 ↩

  9. 出处:「Chapter 2: Fundamentals of System Programming」第 157–164 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:158,搜「resolved when」)。 ↩

  10. 出处:「Chapter 2: Fundamentals of System Programming」第 165–169 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:167,搜「Linking Format」)。 ↩

  11. 出处:「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

  12. 出处:「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

  13. 出处:「Chapter 2: Fundamentals of System Programming」第 283–285 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:283,搜「not yet bound」)。 ↩

  14. 出处:「Chapter 2: Fundamentals of System Programming」第 296–326 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:309,搜「linux-vdso」)。 ↩

  15. 出处:「Chapter 2: Fundamentals of System Programming」第 331–336 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:336,搜「libc.so」)。 ↩

  16. 出处:「Chapter 2: Fundamentals of System Programming」第 355–383 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:358,搜「gnu_get_libc_version」)。 ↩

  17. 出处:「Chapter 2: Fundamentals of System Programming」第 385–405 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:390,搜「significantly different」)。 ↩

  18. 出处:「Chapter 2: Fundamentals of System Programming」第 392–396 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:393,搜「Section 2」)。 ↩

  19. 出处:「Chapter 2: Fundamentals of System Programming」第 403–405 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:438,搜「trap instruction」)。 ↩

  20. 出处:「Chapter 2: Fundamentals of System Programming」第 407–410 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:409,搜「sysenter」)。 ↩

  21. 出处:「Chapter 2: Fundamentals of System Programming」第 411–420 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:418,搜「450」)。 ↩

  22. 出处:「Chapter 2: Fundamentals of System Programming」第 421–428 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:424,搜「Register method」)。 ↩

  23. 出处:「Chapter 2: Fundamentals of System Programming」第 429–430 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:429,搜「six parameters」)。 ↩

  24. 出处:「Chapter 2: Fundamentals of System Programming」第 432–436 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:433,搜「wrapper functions」)。 ↩ ↩2

  25. 出处:「Chapter 2: Fundamentals of System Programming」第 439–441 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:439,搜「thin」)。 ↩

  26. 出处:「Chapter 2: Fundamentals of System Programming」第 442–450 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:450,搜「decides which one」)。 ↩

  27. 出处:「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

  28. 出处:「Chapter 2: Fundamentals of System Programming」第 452–471 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:453,搜「steps that take place」)。 ↩

  29. 出处:「Chapter 2: Fundamentals of System Programming」第 556–560 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:557,搜「negative number」)。 ↩

  30. 出处:「Chapter 2: Fundamentals of System Programming」第 561–565 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:563,搜「static variable named errno」)。 ↩

  31. 出处:「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

  32. 出处:「Chapter 2: Fundamentals of System Programming」第 566–571 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:568,搜「ERRORS section」)。 ↩

  33. 出处:「Chapter 2: Fundamentals of System Programming」第 609–615 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:612,搜「size_t」)。 ↩

  34. 出处:「Chapter 2: Fundamentals of System Programming」第 628–632 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:630,搜「EFAULT」)。 ↩

  35. 出处:「Chapter 2: Fundamentals of System Programming」第 616–627 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:627,搜「segmentation fault」)。 ↩

  36. 出处:「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」)。 ↩

  37. 出处:「Chapter 2: Fundamentals of System Programming」第 680–687 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:682,搜「locale aware」)。 ↩

  38. 出处:「Chapter 2: Fundamentals of System Programming」第 688–729 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:722,搜「File name too long」)。 ↩

  39. 出处:「Chapter 2: Fundamentals of System Programming」第 730–737 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:735,搜「pointer to a string」)。 ↩

  40. 出处:「Chapter 2: Fundamentals of System Programming」第 759–775 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:761,搜「four different categories」)。 ↩

  41. 出处:「Chapter 2: Fundamentals of System Programming」第 776–777 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:777,搜「read its man page」)。 ↩

  42. 出处:「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。 ↩

  43. 出处:「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 段。 ↩

  44. 出处:「Chapter 2: Fundamentals of System Programming」第 842–856 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:842,搜「Feature Test Macro Requirements」)。 ↩

  45. 出处:「Chapter 2: Fundamentals of System Programming」第 883–896 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:884,搜「2.10」)。 ↩

  46. 出处:「Chapter 2: Fundamentals of System Programming」第 897–906 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:899,搜「-D option」)。 ↩

  47. 出处:「Chapter 2: Fundamentals of System Programming」第 918–934 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:932,搜「logical-AND」)。 ↩

  48. 出处:「Chapter 2: Fundamentals of System Programming」第 935–950 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:937,搜「_POSIX_SOURCE」)。 ↩

  49. 出处:「Chapter 2: Fundamentals of System Programming」第 951–953 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:951,搜「feature_test_macros」)。 ↩

  50. 出处:「Chapter 2: Fundamentals of System Programming」第 964–970 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:968,搜「_POSIX_NAME_MAX」)。 ↩

  51. 出处:「Chapter 2: Fundamentals of System Programming」第 971–980 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:974,搜「divides system limits」)。 ↩

  52. 出处:「Chapter 2: Fundamentals of System Programming」第 981–984 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:982,搜「pathconf」)。 ↩ ↩2

  53. 出处:「Chapter 2: Fundamentals of System Programming」第 986–1010 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:993,搜「MM/DD」)。 ↩

  54. 出处:「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

  55. 出处:「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」)。 ↩

  56. 出处:「Chapter 2: Fundamentals of System Programming」第 1074–1084 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1081,搜「argc ≥ 1」)。 ↩

  57. 出处:「Chapter 2: Fundamentals of System Programming」第 1092–1112 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1096,搜「showargs」)。 ↩

  58. 出处:「Chapter 2: Fundamentals of System Programming」第 1131–1147 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1133,搜「argv[i] == NULL」)。 ↩

  59. 出处:「Chapter 2: Fundamentals of System Programming」第 1151–1155 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1153,搜「three different ways」)。 ↩

  60. 出处:「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」)。 ↩

  61. 出处:「Chapter 2: Fundamentals of System Programming」第 1223–1274 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1266,搜「printenv」)。 ↩

  62. 出处:「Chapter 2: Fundamentals of System Programming」第 1275–1294 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1292,搜「doesn't support it」)。 ↩ ↩2

  63. 出处:「Chapter 2: Fundamentals of System Programming」第 1315–1338 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1322,搜「usage: %s file1 file2」)。 ↩

  64. 出处:「Chapter 2: Fundamentals of System Programming」第 1349–1355 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1355,搜「contains the entire pathname」)。 ↩

  65. 出处:「Chapter 2: Fundamentals of System Programming」第 1361–1364 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1363,搜「relatively portable ways」)。 ↩

  66. 出处:「Chapter 2: Fundamentals of System Programming」第 1405–1409 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1407,搜「basetype」)。 ↩

  67. 出处:「Chapter 2: Fundamentals of System Programming」第 1413–1425 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1419,搜「libgen.h」)。 ↩

  68. 出处:「Chapter 2: Fundamentals of System Programming」第 1486–1493 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1490,搜「getopt_long」)。 ↩

  69. 出处:「Chapter 2: Fundamentals of System Programming」第 1523–1525 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1507,搜「extern char」)。 ↩

  70. 出处:「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」)。 ↩

  71. 出处:「Chapter 2: Fundamentals of System Programming」第 1555–1561 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1558,搜「returns -1」) · :1560(搜「rearranges」)。 ↩

  72. 出处:「Chapter 2: Fundamentals of System Programming」第 1562–1568 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1563,搜「two parts」)。 ↩

  73. 出处:「Chapter 2: Fundamentals of System Programming」第 1682–1701 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1684,搜「No error checking」)。 ↩

  74. 出处:「Chapter 2: Fundamentals of System Programming」第 1702–1710 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1703,搜「don't set the errno」)。 ↩

  75. 出处:「Chapter 2: Fundamentals of System Programming」第 1708–1710 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1710,搜「throw-away code」)。 ↩ ↩2

  76. 出处:「Chapter 2: Fundamentals of System Programming」第 1716–1781 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1719,搜「long strtol」) · :1741(搜「errno = 0」)。 ↩

  77. 出处:「Chapter 2: Fundamentals of System Programming」第 1782–1799 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1785,搜「100000000000000」) · :1799(搜「octal」)。 ↩

  78. 出处:「Chapter 2: Fundamentals of System Programming」第 1803–1839 段(text/08-ch02-chapter-2-fundamentals-of-system-programming.txt:1836,搜「wouldn't recommend」)。 ↩

On this page