glibc 库解析
GNU C Library 目标是为 Linux 内核操作系统提供用户态的 C 标准库;由于 C 标准库无法直接调用内核,所以 glibc 还实现了 POSIX.1 接口,用于与内核通信。
我们通过关注一个最常见的 write() 函数实现来解析该库,这个源码非常简洁,只有一个函数和几行宏调用:
#include <unistd.h>
#include <sysdep-cancel.h>
/* Write NBYTES of BUF to FD. Return the number written, or -1. */
ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (write, fd, buf, nbytes);
}
libc_hidden_def (__libc_write)
weak_alias (__libc_write, __write)
libc_hidden_weak (__write)
weak_alias (__libc_write, write)
libc_hidden_weak (write)
动态库的符号导出
write.c 中只定义了 __libc_write() 函数,它是如何对外提供 write() 函数的呢?答案是使用别名将 write() 映射到 __libc_write(),并通过符号版本控制将 write 符号导出。接下来详细介绍其内部实现。
write.c 源码中用到了 3 个宏定义:
libc_hidden_def用于对外隐藏函数符号__libc_write。weak_alias用于创建 weak 别名,即__write等效于__libc_write。libc_hidden_weak是前两者的合并,对外隐藏__write符号,并创建__write_hidden别名。
总的来说,__libc_write 函数使用系统调用执行,并对外隐藏及其 write 别名。既然 write 符号被隐藏,那 libc.so 是如何对外提供 write 函数的呢?答案是 write 会通过符号版本控制文件 libc.map 再次导出。
动态库的符号导出由散落在各个文件夹中的 Versions 文件定义。比如符号 write :
libc {
GLIBC_2.0 {
......
write;
}
......
}
第一个 libc 表示这是 libc.so 动态库导出的符号;第二个 GLIBC_2.0 表示该符号的版本。但实际读取 libc.so 动态库的符号,可以发现 write 的版本号为 GLIBC_2.2.5,与上述定义不符:
$ readelf --dyn-syms /usr/lib/x86_64-linux-gnu/libc.so.6 | grep write
......
2948: 000000000011c560 157 FUNC WEAK DEFAULT 17 write@@GLIBC_2.2.5
......
这是因为 glibc 在编译时通过 awk 脚本修改了最低符号版本的定义。 最低符号版本由散落在文件夹中的 shlib-versions 文件定义。对于 x86_64 架构,最低为 GLIBC_2.2.5:
# DEFAULT Earliest symbol set
# --------------- ------------------------------
DEFAULT GLIBC_2.2.5
ld=ld-linux-x86-64.so.2
下图是 Versions 和 shlib-versions 生成符号版本控制文件 libc.map 流程;该文件是 GCC 编译动态库 libc.so 时,生成导出符号所需的配置文件:
上述流程在 Makeconfig 中定义。主要关注流程中的 Versions.all 文件,它将所有低于 GLIBC_2.2.5 的版本都映射到 GLIBC_2.2.5;高于 GLIBC_2.2.5 的版本则保持不变;最终生成的 libc.map 如下:
GLIBC_2.2.5 {
global:
......
write;
local:
*;
};
......
系统调用实现
回到 write 函数的实现,其调用 SYSCALL_CANCEL 宏,而该宏的调用如下图:
SYSCALL_CANCEL 调用到 __syscall_cancel_arch(),后者又由两部分组成:
long int
__syscall_cancel_arch (......)
{
......
__syscall_do_cancel();
long int result = INTERNAL_SYSCALL_NCS_CALL (nr, a1, a2, a3, a4, a5, a6
__SYSCALL_CANCEL7_ARG7);
......
}
- __syscall_do_cancel() :用汇编实现了取消点:
ENTRY (__syscall_cancel_arch)
.globl __syscall_cancel_arch_start
__syscall_cancel_arch_start:
/* if (*cancelhandling & CANCELED_BITMASK)
__syscall_do_cancel() */
mov (%rdi),%eax
testb $TCB_CANCELED_BITMASK, (%rdi)
jne __syscall_do_cancel
/* Issue a 6 argument syscall, the nr [%rax] being the syscall
number. */
mov %rdi,%r11
mov %rsi,%rax
mov %rdx,%rdi
mov %rcx,%rsi
mov %r8,%rdx
mov %r9,%r10
mov 8(%rsp),%r8
mov 16(%rsp),%r9
mov %r11,8(%rsp)
syscall
.globl __syscall_cancel_arch_end
__syscall_cancel_arch_end:
ret
END (__syscall_cancel_arch)
- internal_syscall3() :代表输入参数个数为 3 的直接系统调用,使用内联汇编实现:
#define internal_syscall3(number, arg1, arg2, arg3) \
({ \
unsigned long int resultvar; \
TYPEFY (arg3, __arg3) = ARGIFY (arg3); \
TYPEFY (arg2, __arg2) = ARGIFY (arg2); \
TYPEFY (arg1, __arg1) = ARGIFY (arg1); \
register TYPEFY (arg3, _a3) asm ("rdx") = __arg3; \
register TYPEFY (arg2, _a2) asm ("rsi") = __arg2; \
register TYPEFY (arg1, _a1) asm ("rdi") = __arg1; \
asm volatile ( \
"syscall\n\t" \
: "=a" (resultvar) \
: "0" (number), "r" (_a1), "r" (_a2), "r" (_a3) \
: "memory", REGISTERS_CLOBBERED_BY_SYSCALL); \
(long int) resultvar; \
})
总的来说,系统调用需要使用汇编的特殊指令实现。
系统调用号
再次回到 write.c 的系统调用 SYSCALL_CANCEL,它的第一个参数是 write,这是一个由 linux kernel 定义的宏定义,下图是其引用链:
查看 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 文件即可看到 __NR_write 的调用号为 1:
#ifndef _ASM_UNISTD_64_H
#define _ASM_UNISTD_64_H
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
......
#endif /* _ASM_UNISTD_64_H */
但这里又有一个问题,write 是如何映射到 __NR_write 的呢?其实是 SYSCALL_CANCEL 宏在调用到 __SYSCALL_CANCEL3 时做了一个 name --> __NR_##name 的变换:
#define __SYSCALL_CANCEL3(name, a1, a2, a3) \
__syscall_cancel (__SSC (a1), __SSC (a2), __SSC (a3), 0, 0, 0, \
__SYSCALL_CANCEL7_ARG __NR_##name)
内核是如何生成系统调用号
已知 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 由 linux-libc-dev 包提供,该包又由内核代码提供:
$ dpkg -S /usr/include/x86_64-linux-gnu/asm/unistd_64.h
linux-libc-dev:amd64: /usr/include/x86_64-linux-gnu/asm/unistd_64.h
$ apt info linux-libc-dev | grep Source:
Source: linux
但在源码中并没有 unistd_64.h 文件,该文件是通过 syscall_64.tbl 生成的:
参考资料
更多推荐


所有评论(0)