
一句话总结semget和semctl的 musl 实现核心就在处理三个脏活内核类型不匹配、时间戳64位扩展、以及某些架构上的模式位hack。一、semget一行代码背后的类型陷阱int semget(key_t key, int n, int fl) { if (n USHRT_MAX) return __syscall_ret(-EINVAL); return syscall(SYS_semget, key, n, fl); }为什么要手动检查n USHRT_MAXPOSIX 规定n的类型是unsigned short最大值USHRT_MAX 65535。但 Linux 内核的struct semid_ds中sem_nsems字段用的是int类型。内核不会帮你检查这个边界所以 musl 在用户态拦了一刀你传 100000对不起我直接返回-EINVAL根本不进系统调用。这是一个经典的用户态防御性检查——内核 sloppylibc 补位。二、semctl可变参数 三层 Hack这才是重头戏。2.1 union semun —— POSIX 留下的遗产union semun { int val; struct semid_ds *buf; unsigned short *array; };这个联合体是 System V IPC 的历史包袱。不同命令需要不同类型的参数命令使用哪个成员含义SETVALval设置某个信号量的值GETALL/SETALLarray批量读/写所有信号量值IPC_SET/IPC_STATbuf设置/获取整个 semid_ds 结构musl 用va_list按需提取参数不需要的命令就传{0}。2.2 第一层 HackIPC_TIME64 —— 时间戳的 64 位扩展#if IPC_TIME64 struct semid_ds out, *orig; if (cmdIPC_TIME64) { out (struct semid_ds){0}; orig arg.buf; arg.buf out; } #endif问题老的semid_ds结构里sem_otime和sem_ctime是long32位。新接口要支持 64 位时间戳。musl 的做法造一个临时结构out清零把arg.buf指向out而不是用户传的缓冲区调用内核内核把结果写到out里调用返回后把out拷贝回用户的缓冲区用IPC_HILO宏处理高低32位的拆分#if IPC_TIME64 if (r 0 (cmdIPC_TIME64)) { arg.buf orig; *arg.buf out; IPC_HILO(arg.buf, sem_otime); IPC_HILO(arg.buf, sem_ctime); } #endif本质上是用户态模拟了一次 64 位时间戳的读写转换内核根本不知道这事。2.3 第二层 HackSYSCALL_IPC_BROKEN_MODE —— 模式位的位移#ifdef SYSCALL_IPC_BROKEN_MODE struct semid_ds tmp; if (cmd IPC_SET) { tmp *arg.buf; tmp.sem_perm.mode * 0x10000U; arg.buf tmp; } #endif这是什么鬼某些架构主要是小端序上内核期望mode字段左移 16 位后再传入。musl 的做法调用前mode * 0x10000左移16位调用后mode 16右移16位恢复#ifdef SYSCALL_IPC_BROKEN_MODE if (r 0) switch (cmd | IPC_TIME64) { case IPC_STAT: case SEM_STAT: case SEM_STAT_ANY: arg.buf-sem_perm.mode 16; } #endif为什么只在小端序定义#if __BYTE_ORDER ! __BIG_ENDIAN #undef SYSCALL_IPC_BROKEN_MODE #endif因为只有小端序架构的内核有这个 bug。大端序不需要这个 hack直接#undef掉。这是 musl 对内核 ABI 缺陷的用户态补丁你在 glibc 里看不到这么裸露的 hack。2.4 两种系统调用路径#ifndef SYS_ipc return syscall(SYS_semctl, id, num, IPC_CMD(cmd), arg.buf); #else return syscall(SYS_ipc, IPCOP_semctl, id, num, IPC_CMD(cmd), arg.buf); #endif架构系统调用方式普通syscall(SYS_semctl, ...)直接传arg.bufSYS_ipc 架构syscall(SYS_ipc, IPCOP_semctl, ..., arg.buf)传指针的指针后者是因为某些架构如 MIPS的 IPC 系统调用约定要求参数以特殊方式传递。三、一张图总结 semctl 的参数流转用户调用 semctl(id, num, IPC_SET, buf) │ ▼ va_arg 提取 buf → arg.buf buf │ ▼ ┌─ IPC_TIME64? ──是──→ arg.buf 指向临时 outorig 保存原 buf │ ├─ BROKEN_MODE 且 IPC_SET? ──是──→ mode 左移16位用 tmp 包装 │ ▼ 系统调用 │ ▼ ┌─ BROKEN_MODE 且 读操作? ──是──→ mode 右移16位恢复 │ └─ IPC_TIME64? ──是──→ out 拷贝回 orig高低位拆分 │ ▼ 返回结果四、musl vs glibc设计哲学差异维度muslglibc代码量~150 行搞定分散在多个文件逻辑类似但封装更深Hack 暴露程度全部暴露在源代码里封装在__syscall内部可读性极高一个文件看完需要跳转多个文件维护成本低逻辑集中高碎片化musl 的哲学把所有和内核打交道的脏活明明白白写在你眼前。五、关键 Takeaway知识点一句话n USHRT_MAX检查内核类型不匹配用户态补位union semunSystem V IPC 的历史遗留按命令选成员IPC_TIME64用户态模拟 64 位时间戳内核无感知SYSCALL_IPC_BROKEN_MODE小端序架构的内核 bug用户态 hack 修复SYS_ipc分支某些架构的 IPC 调用约定不同