GNU C Library 目标是为 Linux 内核操作系统提供用户态的 C 标准库;由于 C 标准库无法直接调用内核,所以 glibc 还实现了 POSIX.1 接口,用于与内核通信。

kernel
libc.so
kernel
POSIX.1
ISO C

我们通过关注一个最常见的 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 个宏定义:

  1. libc_hidden_def 用于对外隐藏函数符号 __libc_write
  2. weak_alias 用于创建 weak 别名,即 __write 等效于 __libc_write
  3. 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

下图是 Versionsshlib-versions 生成符号版本控制文件 libc.map 流程;该文件是 GCC 编译动态库 libc.so 时,生成导出符号所需的配置文件:

Versions
Versions.v.i
Versions.v
scripts/versionlist.awk
Versions.def
shlib-versions
shlib-versions.v.i
shlib-versions.v
scripts/soversions.awk
soversions.i
scripts/firstversions.awk
Versions.all
scripts/versions.awk
libc.map

上述流程在 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 宏,而该宏的调用如下图:

sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S
sysdeps/unix/sysv/linux/x86_64/sysdep.h
sysdeps/unix/sysdep.h
sysdeps/unix/sysv/linux/syscall_cancel.c
nptl/cancellation.c
__syscall_do_cancel()
internal_syscall4()
INTERNAL_SYSCALL_NCS
__INTERNAL_SYSCALL_NCS3
__INTERNAL_SYSCALL_DISP
INTERNAL_SYSCALL_NCS_CALL
__syscall_cancel_arch()
__internal_syscall_cancel()
__syscall_cancel()
__SYSCALL_CANCEL3
__SYSCALL_CANCEL_DISP
__SYSCALL_CANCEL_CALL
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);
  ......
}
  1. __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)
  1. 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 定义的宏定义,下图是其引用链:

kernel
glibc
/usr/include/x86_64-linux-gnu/asm/unistd_64.h
/usr/include/x86_64-linux-gnu/asm/unistd.h
sysdeps/unix/sysv/linux/sys/syscall.h
sysdeps/unix/sysdep.h
sysdeps/unix/sysv/linux/sysdep-cancel.h

查看 /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.hlinux-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 生成的:

arch/x86/entry/syscalls/Makefile
arch/x86/include/generated/uapi/asm/unistd_64.h
arch/x86/entry/syscalls/syscall_64.tbl
scripts/syscallhdr.sh

参考资料

  1. 【主页】The GNU C Library
  2. 【主页】The GNU C Library
  3. 【源码】glibc
  4. 【源码】【镜像】glibc
  5. 【下载】gnu/libc
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐