errno是线程安全的吗? 假设有A, B两个线程都执行系统调用, 其中A返回EIO, B返回EAGAIN, 在判断返回值时是否会引起混淆?
简单的通过man errno就可以获取答案: errno is thread-local; setting it in one thread does not affect its value in any other thread. 但是errno究竟是如何实现的? 为什么errno还可能是一个宏? 带着疑问我们来研究下glibc. 官网下载到最新的是2.25版本的源码, 我们就以glibc-2.25为例一探究竟.先看对外暴露的stdlib/errno.h:
1 #include2 #undef __need_Emath 3 #ifndef errno 4 extern int errno; 5 #endif
这里的注释指明两点:
1. bits/errno.h是系统相关头文件, 在该文件中会测试__need_Emath与_ERRNO_H宏. 2. 如果bits/errno.h未定义errno为宏则声明外部变量errno.sysdeps/unix/sysv/linux/bits/errno.h中将其定义为函数:
1 #ifdef _ERRNO_H 2 # ifndef __ASSEMBLER__ 3 extern int *__errno_location (void) __THROW __attribute__ ((__const__)); 4 # if !defined _LIBC || defined _LIBC_REENTRANT 5 # define errno (*__errno_location ()) 6 # endif 7 # endif 8 #endif
搞清楚errno的定义后再来看看errno的修改. 由于不同架构系统调用部分相同部分不同, glibc使用脚本来动态生成系统调用函数的封装, sysdeps/unix/make-syscalls.sh即生成函数封装的脚本. 它会先去读取syscalls.list保存在calls变量中, 通过sed将注释行与空行删除, 将得到的文件按行输入(读入的前三个参数分别为file caller rest)并判断对应架构目录下是否存在$file.c $file.S $caller.c $caller.S(如果$caller不为-)文件中一个, 如果有则记录在calls变量中. 接下来根据系统调用类型及参数配置不同参数, 最后将其输出, 注意line 256开始的宏定义与包含的文件. 此处有点不明白, 输出的文件是怎么确定的?
make-syscalls.sh脚本输出的信息有何作用? 上文中line 256可以解答这个问题. 先来看下系统调用的模板(defined in sysdeps/unix/syscall-template.S):1 #define T_PSEUDO(SYMBOL, NAME, N) PSEUDO (SYMBOL, NAME, N) 2 #define T_PSEUDO_END(SYMBOL) PSEUDO_END (SYMBOL) 3 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) 4 ret 5 T_PSEUDO_END (SYSCALL_SYMBOL)
syscall-template.S定义了一组宏用于定义系统调用的接口, 这里仅分析最常见的情况.
看下以PSEUDO开头命名的宏(defined in sysdeps/unix/sysv/linux/arm/sysdep.h):1 #undef PSEUDO 2 #define PSEUDO(name, syscall_name, args) \ 3 .text; \ 4 ENTRY (name); \ 5 DO_CALL (syscall_name, args); \ 6 cmn r0, $4096; 7 #undef PSEUDO_END 8 #define PSEUDO_END(name) \ 9 SYSCALL_ERROR_HANDLER; \ 10 END (name)
因以PSEUDO开头命名的宏较多, 此处仅分析下PSEUDO与PSEUDO_END, 可见两个宏需成对使用, 分别用于系统调用与错误返回, 继续分析DO_CALL(defined in sysdeps/unix/sysv/linux/arm/sysdep.h):
1 #undef DO_CALL 2 #define DO_CALL(syscall_name, args) \ 3 DOARGS_##args; \ 4 ldr r7, =SYS_ify (syscall_name); \ 5 swi 0x0; \ 6 UNDOARGS_##args
DO_CALL宏有三条注释, 分别说明:
1. ARM EABI用户接口将系统调用号放在R7中, 而非swi中传递. 这种方式更加高效, 因为内核无需从内存中获取调用号, 这对于指令cache与数据cache分开的架构比较麻烦. 因此swi中必须传递0. 2. 内核通过R0-R6共传递7个参数, 而编译器通常只使用4个参数寄存器其余以入栈方式传参(见AAPCS), 此处需要做转换防止栈帧毁坏并保证内核正确获取参数. 3. 由于缓存系统调用号在发生系统调用时必须保存并恢复R7. 根据注释理解代码就方便多了, 先保存R7并将参数传递给对应寄存器, 将系统调用号传递给R7并调用swi 0x0, 最后恢复寄存器. DOARGS_#args根据传入args值不同展开为不同的宏(都定义在同一文件下), 此处仅分析DOARGS_7情况(UNDOARGS_#args类似, 不展开分析):1 #undef DOARGS_7 2 #define DOARGS_7 \ 3 .fnstart; \ 4 mov ip, sp; \ 5 push {r4, r5, r6, r7}; \ 6 cfi_adjust_cfa_offset (16); \ 7 cfi_rel_offset (r4, 0); \ 8 cfi_rel_offset (r5, 4); \ 9 cfi_rel_offset (r6, 8); \ 10 cfi_rel_offset (r7, 12); \ 11 .save { r4, r5, r6, r7 }; \ 12 ldmia ip, {r4, r5, r6}
先将当前栈指针保存在IP中, 将R4-R7依次入栈, 最后通过IP将已经入栈的参数传递给R4-R7. 中间以cfi开头的宏都是伪指令(defined in sysdeps/generic/sysdep.h), 用于debugger分析程序调用间寄存器状态, 不详细分析了, 具体可参见(http://dwarfstd.org/doc/DWARF5.pdf).
SYS_ify宏(defined in sysdeps/unix/sysv/linux/arm/sysdep.h)用于拼接字符串生成对应的调用号(生成的即是内核定义的系统调用号的宏):#define SYS_ify(syscall_name) (__NR_##syscall_name)
再回头看PSEUDO_END, 其展开即调用SYSCALL_ERROR_HANDLER(sysdeps/unix/sysv/linux/arm/sysdep.h)然后声明函数结束. SYSCALL_ERROR_HANDLER根据不同预处理宏有不同定义, 此处仅分析使用libc的errno且架构不支持THUMB_INTERWORK情况:
1 #define SYSCALL_ERROR_HANDLER \ 2 __local_syscall_error: \ 3 push { lr }; \ 4 cfi_adjust_cfa_offset (4); \ 5 cfi_rel_offset (lr, 0); \ 6 push { r0 }; \ 7 cfi_adjust_cfa_offset (4); \ 8 bl PLTJMP(C_SYMBOL_NAME(__errno_location)); \ 9 pop { r1 }; \ 10 cfi_adjust_cfa_offset (-4); \ 11 rsb r1, r1, #0; \ 12 str r1, [r0]; \ 13 mvn r0, #0; \ 14 POP_PC;
代码还是比较简单的, 首先将LR压栈, 再将R0压栈(注意此时R0为系统调用返回值). 然后获取errno的地址, PLTJMP宏表明__errno_location符号是由程序链接表指定而非静态生成的. 由于函数返回值保存在R0, 出栈时使用R1保存系统调用返回值, 又系统调用返回值为复数, 此处再做一次减法取正, 再将其保存在R0给定的地址上(errno). 最后将R0设置为-1, 将LR出栈并跳转.
待续......