字数 2234,阅读大约需 12 分钟
第十章:库函数实现
库函数是用户程序与操作系统之间的桥梁,提供了便捷的编程接口。Linux 0.12实现了基本的C标准库函数,包括系统调用封装、字符串操作、内存分配等。虽然功能相对简单,但这些库函数展现了系统编程的精髓:如何用最少的代码实现最基本的功能。
10.1 系统调用封装
lib目录下的C文件实现了系统调用的用户空间封装。这些文件使用_syscall宏定义系统调用接口,让用户程序可以像调用普通C函数一样调用系统调用。
_syscall宏的实现
_syscall宏是系统调用封装的核心机制。它通过内嵌汇编将系统调用号放入eax、参数放入ebx/ecx/edx/esi/edi,执行int $0x80触发系统调用,然后检查eax返回值:成功(>=0)直接返回,失败(<0)将错误码取反存入errno并返回-1。宏按参数数量分为_syscall0到_syscall3等变体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
/* include/unistd.h - 系统调用宏定义 */
/* 无参数版本 */
#define _syscall0(type, name) \
type name(void) { \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) : "0" (__NR_##name) : "memory"); \
if (__res >= 0) return (type) __res; \
errno = -__res; return -1; \
}
/* 1个参数版本: 参数通过ebx传递 */
#define _syscall1(type, name, atype, a) \
type name(atype a) { \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) : "0" (__NR_##name), "b" ((long)(a)) : "memory"); \
if (__res >= 0) return (type) __res; \
errno = -__res; return -1; \
}
/* 2个参数版本: ebx=参数1, ecx=参数2 */
/* 3个参数版本: ebx, ecx, edx */
/* 系统调用号定义: __NR_exit=1, __NR_fork=2, __NR_read=3, ... */
系统调用封装示例
使用_syscall宏可以非常简洁地定义系统调用接口。_exit和close只需一行宏调用即可完成封装。open因为mode参数可选,需要手动处理可变参数。wait是waitpid(pid=-1)的简单包装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
/* lib/_exit.c */
#define __LIBRARY__
#include <unistd.h>
_syscall1(int, _exit, int, status) /* eax=1(__NR_exit), ebx=status */
/* lib/close.c */
_syscall1(int, close, int, fd) /* eax=6(__NR_close), ebx=fd */
/* lib/open.c - 手动实现(可选的mode参数) */
int open(const char *filename, int flag, ...)
{
va_list arg;
va_start(arg, flag);
int res;
__asm__ ("int $0x80"
: "=a" (res)
: "0" (__NR_open), "b" (filename), "c" (flag),
"d" (va_arg(arg, int))
: "memory");
va_end(arg);
if (res >= 0) return res;
errno = -res; return -1;
}
/* lib/wait.c */
_syscall3(pid_t, waitpid, pid_t, pid, int *, stat_addr, int, options)
pid_t wait(int *stat_addr) { return waitpid(-1, stat_addr, 0); }
execve特殊实现
execve()比较特殊,需要传递复杂的参数数组,不使用_syscall宏。
1 2 3 4 5 6 7 8 9 10 11 12
/* lib/execve.c - 执行程序 */
int execve(const char *file, char **argv, char **envp)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_execve), "b" (file), "c" (argv), "d" (envp)
: "memory");
if (__res >= 0) return (int) __res; /* 成功时不返回 */
errno = -__res; return -1;
}
/* execve成功时内核修改返回地址指向新程序入口,不会返回到用户态调用点 */
系统调用的调用约定
1 2 3 4 5 6 7
Linux 0.12系统调用约定:
- 系统调用号: eax - 返回值: eax (>=0成功, <0错误码)
- 参数: ebx, ecx, edx, esi, edi (最多5个)
示例: read(fd, buf, count)
eax=3(__NR_read), ebx=fd, ecx=buf, edx=count
→ int 0x80 → system_call() → sys_call_table[3] → sys_read()
10.2 字符串操作函数
string.c实现了基本的字符串操作函数,包括strcpy、strncpy、strcat、strncat、strcmp、strncmp、strchr、strrchr、strlen、strspn、strcspn、strpbrk、strstr、strtok等。这些函数的实现使用了高效的汇编代码,充分利用了x86的字符串指令。
字符串基本操作
字符串函数通过内嵌汇编使用x86专用字符串指令实现高效操作:scasb用于strlen(扫描查找\0)、lodsb+stosb用于strcpy(逐字节复制)、rep movsl用于memcpy(先以4字节为单位批量复制再处理余量)。这些指令比纯C循环快得多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
/* lib/string.c - 字符串操作函数 */
/* strlen: 用scasb扫描\0 */
size_t strlen(const char *s)
{
int __res;
__asm__ ("cld\n\trepne\n\tscasb\n\tnotl %0\n\tdecl %0"
: "=c" (__res) : "D" (s), "a" (0), "0" (0xffffffff) : "di");
return __res;
}
/* strcpy: 用lodsb+stosb逐字节复制 */
char *strcpy(char *dest, const char *src)
{
__asm__ ("cld\n1:\tlodsb\n\tstosb\n\ttestb %%al,%%al\n\tjne 1b"
: : "S" (src), "D" (dest) : "si", "di", "ax", "memory");
return dest;
}
/* memcpy: 先rep movsl复制双字(4字节), 再rep movsb复制余量 */
void *memcpy(void *dest, const void *src, size_t n)
{
int d0, d1, d2;
__asm__ __volatile__ ("cld\n\trep ; movsl\n\tmovl %4,%%ecx\n\trep ; movsb"
: "=&c" (d0), "=&D" (d1), "=&S" (d2)
: "0" (n/4), "g" (n%4), "1" (dest), "2" (src) : "memory");
return dest;
}
/* memset: 用rep stosb填充 */
void *memset(void *s, int c, size_t count)
{
__asm__ ("cld\n\trep\n\tstosb"
: : "a" (c), "D" (s), "c" (count) : "cx", "di", "memory");
return s;
}
10.3 内存分配:malloc/free
malloc.c实现了简单的内存分配器。这个分配器使用brk系统调用扩展进程的数据段,然后将内存划分为块进行管理。
内存块结构与malloc实现
内存分配器将堆空间划分为带头部的块链表,头部记录块大小(最低位用作使用标志)。malloc采用首次适应算法查找空闲块,找到后若块过大则分裂。没有合适的空闲块时通过sbrk扩展堆(至少一页4KB)。free释放后将块按地址排序插入空闲链表,并尝试与相邻空闲块合并以减少碎片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
/* lib/malloc.c - 内存分配器 */
struct mem_block {
size_t size; /* 块大小(最低位: 1=已使用 0=空闲) */
struct mem_block *next;
};
staticstruct mem_block *free_list = NULL;
void *malloc(size_t size)
{
size_t real_size = (size + 3) & ~3; /* 4字节对齐 */
/* 首次适应: 查找足够大的空闲块 */
struct mem_block *prev = NULL, *block = free_list;
while (block) {
if ((block->size & ~1) >= real_size) {
/* 块够大: 必要时分裂 */
if ((block->size & ~1) >= real_size + sizeof(struct mem_block) + 16) {
struct mem_block *new_block =
(struct mem_block *)((char *)block + sizeof(struct mem_block) + real_size);
new_block->size = (block->size & ~1) - real_size - sizeof(struct mem_block);
new_block->next = block->next;
block->size = real_size;
block->next = new_block;
}
/* 从空闲链表移除并标记已使用 */
if (prev) prev->next = block->next;
else free_list = block->next;
block->size |= 1;
return (void *)((char *)block + sizeof(struct mem_block));
}
prev = block;
block = block->next;
}
/* 无合适空闲块: 通过sbrk扩展堆 */
size_t extend = (real_size + sizeof(struct mem_block) < 4096) ? 4096 :
real_size + sizeof(struct mem_block);
block = (struct mem_block *)sbrk(extend);
block->size = (extend - sizeof(struct mem_block)) | 1;
return (void *)((char *)block + sizeof(struct mem_block));
}
void free(void *ptr)
{
if (!ptr) return;
struct mem_block *block = (struct mem_block *)((char *)ptr - sizeof(struct mem_block));
block->size &= ~1; /* 标记空闲 */
/* 按地址排序插入空闲链表 */
/* ... */
/* 与相邻空闲块合并 */
/* 检查block->next和block之前的块是否物理相邻且空闲, 若是则合并 */
}
10.4 字符类型判断
ctype.c定义了_ctype数组,用于字符类型判断。数组的每个元素对应一个字符(0-255),值是该字符类型的位掩码:_U表示大写字母,_L表示小写字母,_D表示数字,_C表示控制字符,_P表示标点,_S表示空白,_X表示十六进制数字。
isalpha(c)宏检查c是否是字母(_U或_L位置位),isdigit(c)检查是否是数字(_D位置位),isspace(c)检查是否是空白(_S位置位)。这种查表方式比逐个比较字符范围高效得多。
toupper(c)和tolower(c)宏将字符转换为大小写。实现简单:对于小写字母减去0x20变为大写,对于大写字母加上0x20变为小写。这利用了ASCII码中大小写字母的对应关系。
10.5 其他库函数
errno.c定义了全局变量errno,用于保存系统调用的错误码。当系统调用失败时,_syscall宏将负的返回值取绝对值赋给errno,然后让系统调用函数返回-1。用户程序检查返回值为-1时,查看errno获取具体错误原因。
write.c实现了write()系统调用封装。与大多数系统调用封装不同,write.c还包含了一个特殊的__write()函数,供内核内部使用(虽然实际上内核很少用到)。
库函数的实现体现了系统编程的艺术:用最少的代码实现基本功能,充分利用硬件特性(如字符串指令),权衡简洁性和效率。虽然功能简单,但这些库函数是Linux 0.12用户程序的基础。

通过本章的学习,我们理解了Linux 0.12的库函数实现:从系统调用封装到字符串操作,从内存分配到字符类型判断。这些库函数虽然简单,但为用户程序提供了便捷的编程接口,是应用开发的基础。
10.6 参考资料
本章内容基于以下资料编写:
C语言标准
• ANSI C (C89) Standard - 定义了标准C库函数接口
Linux 0.12源代码文件
• lib/_exit.c、lib/close.c、lib/open.c、lib/write.c - 系统调用封装 • lib/wait.c - wait/waitpid封装 • lib/execve.c - execve特殊实现 • lib/string.c - 字符串操作函数 • lib/malloc.c - 内存分配器 • lib/ctype.c - 字符类型判断 • lib/errno.c - errno全局变量 • include/unistd.h - 系统调用号和_syscall宏定义 • include/string.h - 字符串函数声明 • include/ctype.h - 字符类型宏定义
"千里之堤,溃于蚁穴。" —— 《韩非子》
夜雨聆风