之前分析errno的实现时有讲过系统调用的实现, 但是写到一半烂尾了, 于是决定重新挖个坑(- -!).
假设我们调用了一个open(), 从pc指向open()入口到pc执行open()的后一条指令中间究竟发生了什么. 首先明确第一点, 当我们调用open()时并不是直接调用系统调用open, 而是调用glibc的封装函数open(). 让我们从头开始一步一步分析.
让我们来看下open()的声明, include/fcntl.h中并未声明该函数, 但它包含了io/fcntl.h, 而后者声明了该函数.1 #ifndef __USE_FILE_OFFSET642 extern int open(const char *__file, int __oflag, ...) __nonnull((1));3 #else /* ! __USE_FILE_OFFSET64 */4 #ifdef __REDIRECT5 extern int __REDIRECT(open, (const char *__file, int __oflag, ...), open64) __nonnull((1));6 #else /* ! __REDIRECT */7 #define open open648 #endif9 #endif
手边只有官网下的glibc-2.25的源码, 没有海思的源码, 好在可以反汇编海思库, 从反汇编结果来看应该是定义了__USE_FILE_OFFSET64且定义了__REDIRECT, 走类似__libc_open64()(defined in sysdeps/unix/sysv/linux/open64.c)的接口(可能不是这个接口, 大致差不多).
1 int __libc_open64(const char *file, int oflag, ...) 2 { 3 int mode = 0; 4 if (__OPEN_NEEDS_MODE (oflag)) 5 { 6 va_list arg; 7 va_start (arg, oflag); 8 mode = va_arg (arg, int); 9 va_end (arg);10 }11 return SYSCALL_CANCEL(open, file, oflag | O_LARGEFILE, mode);12 }
来看下SYSCALL_CANCEL()(defined in sysdeps/unix/sysdep.h)的实现.
1 #define __SYSCALL_CONCAT_X(a, b) a##b 2 #define __SYSCALL_CONCAT(a, b) __SYSCALL_CONCAT_X(a, b) 3 #define __INLINE_SYSCALL_NARGS_X(a, b, c, d, e, f, g, h, n, ...) n 4 #define __INLINE_SYSCALL_NARGS(...) \ 5 __INLINE_SYSCALL_NARGS_X (__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1, 0, ) 6 #define __INLINE_SYSCALL_DISP(b, ...) \ 7 __SYSCALL_CONCAT(b, __INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__) 8 #define INLINE_SYSCALL_CALL(...) \ 9 __INLINE_SYSCALL_DISP(__INLINE_SYSCALL, __VA_ARGS__)10 #define SYSCALL_CANCEL(...) \11 ({ \12 long int sc_ret; \13 if (SINGLE_THREAD_P) \14 sc_ret = INLINE_SYSCALL_CALL(__VA_ARGS__); \15 else \16 { \17 int sc_cancel_oldtype = LIBC_CANCEL_ASYNC(); \18 sc_ret = INLINE_SYSCALL_CALL(__VA_ARGS__); \19 LIBC_CANCEL_RESET(sc_cancel_oldtype); \20 } \21 sc_ret; \22 })
其中SINGLE_THREAD_P()(defined in sysdeps/unix/sysv/linux/arm/sysdep-cancel.h)用于判断是否单线程程序, 在编译glibc时定义__ASSEMBLER__则实现如下:
1 #define SINGLE_THREAD_P \2 LDST_PCREL(ldr, ip, ip, __local_multiple_threads); \3 teq ip, #0
LIBC_CANCEL_ASYNC()/LIBC_CANCEL_RESET()实现没找到(毕竟不是一份源码), 看nptl/cancellation.c的实现应该是用于置位/清零异步取消的标记. 实际的系统调用见INLINE_SYSCALL_CALL()(defined in sysdeps/unix/sysdep.h)的实现, 该宏展开后时__INLINE_SYSCALL*(__VA_ARGS__), 其中*为参数个数. __INLINE_SYSCALL*同样是一组宏, 以__INLINE_SYSCALL3()为例.
1 #define __INLINE_SYSCALL3(name, a1, a2, a3) \ 2 INLINE_SYSCALL(name, 3, a1, a2, a3) 3 INLINE_SYSCALL()(defined in sysdeps/unix/sysv/linux/arm/sysdep.h)是基于架构实现的宏, 在不同平台上有不同实现. 4 #ifndef __ASSEMBLER__ 5 #define LOAD_ARGS_0() 6 #define ASM_ARGS_0 7 #define LOAD_ARGS_1(a1) \ 8 int _a1tmp = (int)(a1); \ 9 LOAD_ARGS_0() \10 _a1 = _a1tmp;11 #define ASM_ARGS_1 ASM_ARGS_0, "r" (_a1)12 #define LOAD_ARGS_2(a1, a2) \13 int _a2tmp = (int)(a2); \14 LOAD_ARGS_1(a1) \15 register int _a2 asm ("a2") = _a2tmp;16 #define ASM_ARGS_2 ASM_ARGS_1, "r" (_a2)17 #if defined(__thumb__)18 #undef INTERNAL_SYSCALL_RAW19 #define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \20 ({ \21 register int _a1 asm ("a1"); \22 int _nametmp = name; \23 LOAD_ARGS_##nr (args) \24 register int _name asm ("ip") = _nametmp; \25 asm volatile ("bl __libc_do_syscall" \26 : "=r" (_a1) \27 : "r" (_name) ASM_ARGS_##nr \28 : "memory", "lr"); \29 _a1; \30 })31 #else /* ARM */32 #undef INTERNAL_SYSCALL_RAW33 #define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \34 ({ \35 register int _a1 asm ("r0"), _nr asm ("r7"); \36 LOAD_ARGS_##nr (args) \37 _nr = name; \38 asm volatile ("swi 0x0 @ syscall " #name \39 : "=r" (_a1) \40 : "r" (_nr) ASM_ARGS_##nr \41 : "memory"); \42 _a1; \43 })44 #endif45 #undef INTERNAL_SYSCALL46 #define INTERNAL_SYSCALL(name, err, nr, args...) \47 INTERNAL_SYSCALL_RAW(SYS_ify(name), err, nr, args)48 #undef INLINE_SYSCALL49 #define INLINE_SYSCALL(name, nr, args...) \50 ({ \51 unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args); \52 if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) \53 { \54 __set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \55 _sys_result = (unsigned int) -1; \56 } \57 (int) _sys_result; \58 })59 #endif
INTERNAL_SYSCALL_RAW()同样有两种实现, 我们只关注ARM指令集的实现. 以LOAD_ARGS_2为例, LOAD_ARGS_*是一组用于加载参数(将参数放入对应寄存器)的宏, 其参数压栈顺序依次为a1(r0), a2(r1), a3(r2), a4(r3), v1(r4), v2(r5), v3(r6), r7记录了参数个数.
乍一看好像没有问题? Hell No! 以上分析是基于未定义__ASSEMBLER__, 即传统ABI, 对于EABI走的是另一套逻辑. 由于未定义INTERNAL_SYSCALL, 默认使用sysdeps/unix/sysdep.h下定义.
1 #ifndef INLINE_SYSCALL2 #define INLINE_SYSCALL(name, nr, args...) __syscall_##name(args)3 #endif
__syscall_##name在代码中完全找不到, 只能猜测是脚本生成的. makefile中有调用make-syscalls.sh来生成嵌套代码, 其使用模板是syscall-template.S, 只需修改几个宏名字即可(这里有个疑问, 其查找的系统调用的模板syscalls.list里并没有open?).
1 echo '#define SYSCALL_NAME $syscall';2 echo '#define SYSCALL_NARGS $nargs';3 echo '#define SYSCALL_SYMBOL $strong';4 echo '#define SYSCALL_CANCELLABLE $cancellable';5 echo '#define SYSCALL_NOERRNO $noerrno';6 echo '#define SYSCALL_ERRVAL $errval';7 echo '#include';
来看下syscall-template.S, 其中调用的T_PSEUDO*宏为PSEUDO*(defined in sysdeps/unix/sysv/linux/arm/sysdep.h)宏的封装. 我们以带返回值的系统调用为例, 分析流程.
1 #if SYSCALL_NOERRNO 2 //无错误返回值的系统调用, 不做校验直接返回 3 T_PSEUDO_NOERRNO(SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) 4 ret_NOERRNO 5 T_PSEUDO_END_NOERRNO(SYSCALL_SYMBOL) 6 #elif SYSCALL_ERRVAL 7 //将错误码返回在结果中的系统调用, 不修改errno 8 T_PSEUDO_ERRVAL(SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) 9 ret_ERRVAL10 T_PSEUDO_END_ERRVAL(SYSCALL_SYMBOL)11 #else12 //常见的系统调用, 如果有错误码, 返回-1并设置errno13 T_PSEUDO(SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)14 ret15 T_PSEUDO_END(SYSCALL_SYMBOL)16 #endif
可见PSEUDO()与PSEUDO_END()是成对使用的. 其中DOARGS_*是参数压栈的宏, 可见小于4个参数时除r7无需压栈(AAPCS要求), r7压栈原因是AEBI要求使用r7传递系统调用号, 大于4个参数才需要压栈. UNDOARGS_*是反作用的宏.
DO_CALL执行完后会比较r0与-4095大小, 根据比较结果跳转. 原因是早期的系统调用使用负值返回错误状态, 但从2.1版本开始内核的一些系统调用成功时也会返回负值(如lseek返回4G以上偏移), 因此glibc与linux协商使用-4095到-1作为错误码, 更大的负值仍作为成功的返回值.插入一句, 在内核目录include/linux/err.h中定义: #define MAX_ERRNO 4095与#define IS_ERR_VALUE(x) unlikely((x) >= (unsigned long)-MAX_ERRNO). 对于需要返回指针或错误码的情况, 可以使用IS_ERR_VALUE()宏来判断. 因为表达式右侧是强制转换为unsigned, 能比-MAX_ERRNO大的只有负数且绝对值小于MAX_ERRNO的负数.1 #undef DOARGS_0 2 #define DOARGS_0 \ 3 .fnstart; \ 4 push {r7}; \ 5 cfi_adjust_cfa_offset (4); \ 6 cfi_rel_offset (r7, 0); \ 7 .save {r7} 8 #undef DOARGS_1 9 #define DOARGS_1 DOARGS_010 #undef DOARGS_211 #define DOARGS_2 DOARGS_012 #undef DOARGS_313 #define DOARGS_3 DOARGS_014 #undef DOARGS_415 #define DOARGS_4 DOARGS_016 #undef DOARGS_517 #define DOARGS_5 \18 .fnstart; \19 push {r4, r7}; \20 cfi_adjust_cfa_offset (8); \21 cfi_rel_offset (r4, 0); \22 cfi_rel_offset (r7, 4); \23 .save {r4, r7}; \24 ldr r4, [sp, #8]25 #undef DOARGS_626 #define DOARGS_6 \27 .fnstart; \28 mov ip, sp; \29 push {r4, r5, r7}; \30 cfi_adjust_cfa_offset (12); \31 cfi_rel_offset (r4, 0); \32 cfi_rel_offset (r5, 4); \33 cfi_rel_offset (r7, 8); \34 .save {r4, r5, r7}; \35 ldmia ip, {r4, r5}36 #undef DOARGS_737 #define DOARGS_7 \38 .fnstart; \39 mov ip, sp; \40 push {r4, r5, r6, r7}; \41 cfi_adjust_cfa_offset (16); \42 cfi_rel_offset (r4, 0); \43 cfi_rel_offset (r5, 4); \44 cfi_rel_offset (r6, 8); \45 cfi_rel_offset (r7, 12); \46 .save {r4, r5, r6, r7}; \47 ldmia ip, {r4, r5, r6}48 #undef DO_CALL49 #define DO_CALL(syscall_name, args) \50 DOARGS_##args; \51 ldr r7, =SYS_ify (syscall_name); \52 swi 0x0; \53 UNDOARGS_##args54 #undef PSEUDO55 #define PSEUDO(name, syscall_name, args) \56 .text; \57 ENTRY(name); \58 DO_CALL(syscall_name, args); \59 cmn r0, $4096;60 #define PSEUDO_RET \61 it cc; \62 RETINSTR(cc, lr); \63 b PLTJMP(SYSCALL_ERROR)64 #undef ret65 #define ret PSEUDO_RET66 #undef PSEUDO_END67 #define PSEUDO_END(name) \68 SYSCALL_ERROR_HANDLER; \69 END (name)
SYSCALL_ERROR_HANDLER(defined in sysdeps/unix/sysv/linux/arm/sysdep.S)是错误处理接口, 其实现也比较诡异(一部分汇编包在另一个文件里).
1 ENTRY (__syscall_error)2 rsb r0, r0, $03 #define __syscall_error __syscall_error_14 #include
sysdeps/unix/arm/sysdep.S中汇编如下, 此处先去除了无用代码(仅分析glibc因此未定义rtld, 使用ARM指令集因此未定义__thumb__). 这段指令的作用是获取errno在TLS中的偏移并赋值, 然后返回.
1 __syscall_error:2 mov r1, r0 /*返回值保存在r1中 */3 GET_TLS (r2) /* 获取tls地址, 保存在r0中 */4 ldr r2, 1f5 2: ldr r2, [pc, r2] /* 获取errno在tls中偏移 */6 str r1, [r0, r2] /* 保存返回值 */7 mvn r0, #0 /* 将r0设为-1 */8 DO_RET(lr)9 1: .word errno(gottpoff) + (. - 2b - PC_OFS)
让我们看下GET_TLS(defined in sysdeps/unix/sysv/linux/arm/sysdep.h)的定义. 该宏将lr保存在传入的TMP中, 调用GET_TLS_BODY, 返回在r0中, 如果TMP为lr本身表明无需保存lr. 获取TLS的办法也很简单, 跳转到固定地址0xFFFF0FE0(具体下文分析).
1 #define GET_TLS_BODY \ 2 mov r0, #0xffff0fff; \ 3 mov lr, pc; \ 4 sub pc, r0, #31 5 #undef GET_TLS 6 #define GET_TLS(TMP) \ 7 .ifnc TMP, lr; \ 8 mov TMP, lr; \ 9 cfi_register (lr, TMP); \10 GET_TLS_BODY; \11 mov lr, TMP; \12 cfi_restore (lr); \13 .else; \14 GET_TLS_BODY; \15 .endif16 #endif
最后来看下反汇编, 印证我们的分析(其实是对着反汇编才看懂代码的).
1 000be680 <__open>: 2 be680: e51fc028 ldr ip, [pc, #-40] ; be6603 be684: e79fc00c ldr ip, [pc, ip] 4 be688: e33c0000 teq ip, #0 5 be68c: e52d7004 push {r7} ; (str r7, [sp, #-4]!) 6 be690: 1a000005 bne be6ac <__open+0x2c> 7 be694: e3a07005 mov r7, #5 8 be698: ef000000 svc 0x00000000 9 be69c: e49d7004 pop {r7} ; (ldr r7, [sp], #4)10 be6a0: e3700a01 cmn r0, #4096 ; 0x100011 be6a4: 312fff1e bxcc lr12 be6a8: eafd661c b 17f20 <__syscall_error>13 be6ac: e92d400f push {r0, r1, r2, r3, lr}14 be6b0: eb007a18 bl dcf18 <__libc_enable_asynccancel>15 be6b4: e1a0c000 mov ip, r016 be6b8: e8bd000f pop {r0, r1, r2, r3}17 be6bc: e3a07005 mov r7, #518 be6c0: ef000000 svc 0x0000000019 be6c4: e1a07000 mov r7, r020 be6c8: e1a0000c mov r0, ip21 be6cc: eb007a41 bl dcfd8 <__libc_disable_asynccancel>22 be6d0: e1a00007 mov r0, r723 be6d4: e49de004 pop {lr} ; (ldr lr, [sp], #4)24 be6d8: e49d7004 pop {r7} ; (ldr r7, [sp], #4)25 be6dc: e3700a01 cmn r0, #4096 ; 0x100026 be6e0: 312fff1e bxcc lr27 be6e4: eafd660d b 17f20 <__syscall_error>28 be6e8: e1a00000 nop ; (mov r0, r0)29 be6ec: e1a00000 nop ; (mov r0, r0)30 00017f20 <__syscall_error>:31 17f20: e2600000 rsb r0, r0, #032 00017f24 <__syscall_error_1>:33 17f24: e1a0c00e mov ip, lr34 17f28: e1a01000 mov r1, r035 17f2c: e3e00a0f mvn r0, #61440 ; 0xf00036 17f30: e1a0e00f mov lr, pc37 17f34: e240f01f sub pc, r0, #3138 17f38: e59f200c ldr r2, [pc, #12] ; 17f4c <__syscall_error_1+0x28>39 17f3c: e79f2002 ldr r2, [pc, r2]40 17f40: e7801002 str r1, [r0, r2]41 17f44: e3e00000 mvn r0, #042 17f48: e12fff1c bx ip43 17f4c: 0011c108 andseq ip, r1, r8, lsl #2
用户态的系统调用封装暂告结束, 我们总结一下即:
1. 系统调用都是通过glibc封装的(有个例外是syscall()函数会使用原生系统调用, 具体不分析了, 可以man syscall查看).2. glibc封装的作用主要是参数入栈, 设置系统调用号, 判断返回值与设置errno.3. 在设置系统调用号时native ABI与AEABI的实现不同, 前者系统调用号传在swi指令中, 后者使用r7传递.4. 根据不同系统调用类型glibc会做不同返回处理, 对于通常系统调用, 其结果保存在errno中(如果失败), errno是线程安全的, 其实现下文详述.让我们先回到swi指令, 执行swi后跳转系统异常, arch/arm/kernel/entry-armv.S中定义了异常向量表. 对于软中断向量表很简单, 直接调转vector_swi(defined in arch/arm/kernel/entry-common.S).
vector_swi()的作用是保存进入内核态时寄存器环境, 根据系统调用号查找系统调用入口, 跳转执行系统调用以及在返回后做错误处理. 其中压栈步骤见注释, 查找系统调用入口时需注意新旧abi的区别, eabi使用r7传递系统调用号而old abi使用swi的参数位传递系统调用号, 执行系统调用后返回(lr)在ret_fast_syscall().1 ENTRY(vector_swi) 2 /** 3 在当前栈上保存用户态寄存器用于返回时恢复现场 4 注意栈缩减大小正好是sizeof(pt_regs), 后文将以该结构访问寄存器 5 * 6 **/ 7 sub sp, sp, #S_FRAME_SIZE 8 stmia sp, {r0 - r12} 9 /** 10 ARM()宏为ARM模式下指令, THUMB()为定义THUMB2_KERNEL时才起效的THUMB模式指令 11 压栈的sp与lr实际为sp_svc与lr_svc 12 其中lr_svc在触发swi时被硬件赋值为swi指令的后一条指令 13 * 14 **/ 15 ARM( add r8, sp, #S_PC ) 16 ARM( stmdb r8, {sp, lr}^ ) 17 THUMB( mov r8, sp ) 18 THUMB( store_user_sp_lr r8, r10, S_SP ) 19 /** 20 依次压栈pc, cpsr, r0, 其中pt_regs->pc保存值与pt_regs->lr相同均为lr_svc 21 spsr_svc在触发swi时被硬件赋值为触发swi时的cpsr 22 在其它异常中需要使用r0保存栈帧, 所以用old r0保存r0, 在swi中r0即old r0 23 * 24 **/ 25 mrs r8, spsr 26 str lr, [sp, #S_PC] 27 str r8, [sp, #S_PSR] 28 str r0, [sp, #S_OLD_R0] 29 /** 30 将fp设置为0, 需定义FRAME_POINTER(默认定义) 31 * 32 **/ 33 zero_fp 34 #ifdef CONFIG_ALIGNMENT_TRAP 35 /** 36 如果定义ALIGNMENT_TRAP(默认定义)则需设置协处理器设置非对齐访问 37 * 38 **/ 39 ldr ip, __cr_alignment 40 ldr ip, [ip] 41 mcr p15, 0, ip, c1, c0 42 #endif 43 /** 44 enable_irq(defined in arch/arm/include/asm/assembler.h)作用 45 将工作模式设置为svc模式 46 提问: 如果未定义TRACE_IRQFLAGS, 软中断时本来就处于svc模式还有必要再设置一次吗? 47 ct_user_exit(defined in arch/arm/kernel/entry-header.S)作用 48 跟踪用户态到内核态的上下文切换(需定义CONTEXT_TRACKING) 49 get_thread_info(defined in arch/arm/kernel/entry-header.S)作用 50 获取当前任务, 其传入的参数tsk(defined in arch/arm/kernel/entry-header.S)等于r9 51 将sp保存在tsk(r9)中并左移13位的结果右移13位(即current_thread_info()的汇编写法) 52 * 53 **/ 54 enable_irq 55 ct_user_exit 56 get_thread_info tsk 57 #if defined(CONFIG_OABI_COMPAT) 58 /** 59 在定义OABI_COMPAT(allow old abi, 即使用eabi但兼容旧abi情况)时需要判断是何种abi 60 * 61 **/ 62 #ifdef CONFIG_ARM_THUMB 63 /** 64 定义ARM_THUMB即支持用户态thumb指令集的二进制程序(默认支持) 65 对于swi定义ARM_THUMB与否不影响结果, 因为swi时cpsr[5]固定为0, r10保存的都是swi指令 66 注意USER宏会定义成对的指令地址, 用于缺页异常处理失败时跳转, 跳转地址为下文的9001f 67 * 68 **/ 69 tst r8, #PSR_T_BIT 70 movne r10, #0 71 USER( ldreq r10, [lr, #-4] ) 72 #else 73 USER( ldr r10, [lr, #-4] ) 74 #endif 75 #ifdef CONFIG_CPU_ENDIAN_BE8 76 /** 77 使用大端字节序, 反转指令 78 * 79 **/ 80 rev r10, r10 81 #endif 82 #elif defined(CONFIG_AEABI) 83 /** 84 纯eabi模型用户态代码会将系统调用号放入scno(r7)中传递下来, 无需处理 85 * 86 **/ 87 #elif defined(CONFIG_ARM_THUMB) 88 tst r8, #PSR_T_BIT 89 addne scno, r7, #__NR_SYSCALL_BASE 90 USER( ldreq scno, [lr, #-4] ) 91 #else 92 USER( ldr scno, [lr, #-4] ) 93 #endif 94 /** 95 tbl(defined in arch/arm/kernel/entry-header.S)为r8 96 sys_call_table(defined in arch/arm/kernel/entry-common.S)为常量数组 97 * 98 **/ 99 adr tbl, sys_call_table100 #if defined(CONFIG_OABI_COMPAT)101 /**102 如果swi参数为0则说明是eabi系统调用, 无需设置参数103 否则需要将系统调用号传递给scno(r7)并获取old abi系统调用表地址104 sys_oabi_call_table(defined in arch/arm/kernel/entry-common.S)为常量数组105 *106 **/107 bics r10, r10, #0xff000000108 eorne scno, r10, #__NR_OABI_SYSCALL_BASE109 ldrne tbl, =sys_oabi_call_table110 #elif !defined(CONFIG_AEABI)111 bic scno, scno, #0xff000000112 eor scno, scno, #__NR_SYSCALL_BASE113 #endif114 local_restart:115 ldr r10, [tsk, #TI_FLAGS]116 stmdb sp!, {r4, r5}117 /**118 是否跟踪系统调用, 如果是走入__sys_trace(使用bne即不会再返回执行后面的代码)119 *120 **/121 tst r10, #_TIF_SYSCALL_WORK122 bne __sys_trace123 /**124 设置lr为ret_fast_syscall, 设置pc为系统调用入口125 注意此处使用ldrcc(carry bit is clear), 若scno大于NR_syscalls则不会执行该指令126 若pc被设置为系统调用入口则执行系统调用, 返回时执行ret_fast_syscall127 只有非公共系统调用才会执行后面的代码128 *129 **/130 cmp scno, #NR_syscalls131 adr lr, BSYM(ret_fast_syscall)132 ldrcc pc, [tbl, scno, lsl #2]133 add r1, sp, #S_OFF134 2:135 /**136 处理非常规(arm私有)与非法(未实现)系统调用137 why(defined in arch/arm/kernel/entry-header.S)为r8138 比较scno与arm私有系统调用基址(__ARM_NR_BASE), 大于走arm_syscall否则走sys_ni_syscall139 arm_syscall()(defined in arch/arm/kernel/traps.c)处理非常规系统调用140 sys_ni_syscall()(defined in kernel/sys_ni.c)处理非法(未实现)系统调用141 当调用返回时跳转至ret_fast_syscall(lr在上文中被赋值)142 *143 **/144 mov why, #0145 cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)146 eor r0, scno, #__NR_SYSCALL_BASE147 bcs arm_syscall?148 b sys_ni_syscall149 #if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)150 /**151 以下代码仅在兼容old abi或不使用eabi时才起效152 我们访问包含swi指令的页失败, 但还未到返回-EFAULT地步153 相反我们重新设置lr, 尝试重入该指令154 *155 **/156 9001:157 sub lr, lr, #4158 str lr, [sp, #S_PC]159 b ret_fast_syscall160 #endif161 ENDPROC(vector_swi)
先来看看__sys_trace()(defined in arch/arm/kernel/entry-common.S), 该函数仅在thread_info->flags的_TIF_SYSCALL_WORK置位时才会进入(即使用strace跟踪系统调用时), 走入slow path.之所以成为slow path的原因: 在syscall_trace_enter中可能发生阻塞, 即可能发生上下文切换.
1 __sys_trace: 2 /** 3 调用syscall_trace_enter, 传递的参数依次为pt_regs与scno 4 * 5 **/ 6 mov r1, scno 7 add r0, sp, #S_OFF 8 bl syscall_trace_enter 9 /**10 修改返回地址(lr)为__sys_trace_return并将r0传递给scno11 r0为syscall_trace_enter返回值, 为保存的scno或-1(系统调用号检查失败或信号挂起)12 之后流程与vector_swi一致, 即压栈r0-r6, r4与r5, 然后查表并调用系统调用13 如果scno大于NR_syscalls, 判断scno是否为-1, 是则调用ret_slow_syscall14 否则为未实现的系统调用, 走入2b(见上文)15 *16 **/17 adr lr, BSYM(__sys_trace_return)18 mov scno, r019 add r1, sp, #S_R0 + S_OFF20 cmp scno, #NR_syscalls21 ldmccia r1, {r0 - r6}22 stmccia sp, {r4, r5}23 ldrcc pc, [tbl, scno, lsl #2]24 cmp scno, #-125 bne 2b26 add sp, sp, #S_OFF27 b ret_slow_syscall
回头看下syscall_trace_enter(defined in arch/arm/kernel/ptrace.c)的返回值. 有两处地方可能返回-1, 一是secure_computing()失败返回-1, 二是tracehook_report_syscall()中修改thread_info->syscall为-1(只有在有信号挂起时才-1).
1 static void tracehook_report_syscall(struct pt_regs *regs, enum ptrace_syscall_dir dir) 2 { 3 unsigned long ip; 4 /ip用于标记syscall的进入与退出, ip = 0为进入, ip = 1为退出 5 ip = regs->ARM_ip; 6 regs->ARM_ip = dir; 7 if (dir == PTRACE_SYSCALL_EXIT) 8 tracehook_report_syscall_exit(regs, 0); 9 else if (tracehook_report_syscall_entry(regs))10 current_thread_info()->syscall = -1;11 regs->ARM_ip = ip;12 }13 asmlinkage int syscall_trace_enter(struct pt_regs *regs, int scno)14 {15 current_thread_info()->syscall = scno;16 if (secure_computing(scno) == -1)17 return -1;18 if (test_thread_flag(TIF_SYSCALL_TRACE))19 tracehook_report_syscall(regs, PTRACE_SYSCALL_ENTER);20 scno = current_thread_info()->syscall;21 if (test_thread_flag(TIF_SYSCALL_TRACEPOINT))22 trace_sys_enter(regs, scno);23 audit_syscall_entry(AUDIT_ARCH_ARM, scno, \24 regs->ARM_r0, regs->ARM_r1, regs->ARM_r2, regs->ARM_r3);25 return scno;26 }
再来看下系统调用表是如何定义的? sys_call_table(defined in arch/arm/kernel/entry-common.S)是一个由calls.S(arch/arm/kernel/calls.S)定义的数组.
1 #define CALL(x) .long x 2 #define ABI(native, compat) native 3 #ifdef CONFIG_AEABI 4 #define OBSOLETE(syscall) sys_ni_syscall 5 #else 6 #define OBSOLETE(syscall) syscall 7 #endif 8 .type sys_call_table, #object 9 ENTRY(sys_call_table)10 #include "calls.S"11 #undef ABI12 #undef OBSOLETE
最后来看下系统调用返回时的接口ret_fast_syscall(defined in arch/arm/kernel/entry-common.S)与ret_slow_syscall()(defined in arch/arm/kernel/entry-common.S).
1 fast_work_pending: 2 /** 3 记录r0, 用于传递系统调用的结果 4 * 5 **/ 6 str r0, [sp, #S_R0+S_OFF]! 7 work_pending: 8 /** 9 do_work_pending()只有两种返回值, 正常返回0或内核异常(返回值为do_signal的返回值)10 正常返回走no_work_pending, 内核异常走local_restart(见上文)11 *12 **/13 mov r0, sp14 mov r2, why15 bl do_work_pending16 cmp r0, #017 beq no_work_pending18 movlt scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)19 ldmia sp, {r0 - r6}20 b local_restart21 ret_fast_syscall:22 UNWIND(.fnstart)23 UNWIND(.cantunwind)24 disable_irq25 /**26 如果thread_info->flags需要调度或信号挂起或需信号处理则走入fast_work_pending27 否则直接恢复用户态环境28 *29 **/30 ldr r1, [tsk, #TI_FLAGS]31 tst r1, #_TIF_WORK_MASK32 bne fast_work_pending33 asm_trace_hardirqs_on34 /**35 arch_ret_to_user()是架构相关代码36 ct_user_exit()是与ct_user_exit()成对调用的上下文跟踪的代码37 *38 **/39 arch_ret_to_user r1, lr40 ct_user_enter41 restore_user_regs fast = 1, offset = S_OFF42 UNWIND(.fnend)43 ret_slow_syscall:44 disable_irq45 ldr r1, [tsk, #TI_FLAGS]46 tst r1, #_TIF_WORK_MASK47 bne work_pending48 no_work_pending:49 asm_trace_hardirqs_on50 arch_ret_to_user r1, lr51 ct_user_enter save = 052 restore_user_regs fast = 0, offset = 0
看下如何从内核态返回到用户态, 以下为未定义THUMB2_KERNEL时restore_user_regs()(defined in arch/arm/kernel/entry-header.S)的实现.
1 .macro restore_user_regs, fast = 0, offset = 0 2 ldr r1, [sp, #\offset + S_PSR] 3 ldr lr, [sp, #\offset + S_PC]! 4 msr spsr_cxsf, r1 5 #if defined(CONFIG_CPU_V6) 6 strex r1, r2, [sp] 7 #elif defined(CONFIG_CPU_32v6K) 8 clrex 9 #endif10 /**11 fast path与slow path区别在于fast path不恢复r0(返回系统调用结果)12 *13 **/14 .if \fast15 ldmdb sp, {r1 - lr}^16 .else17 ldmdb sp, {r0 - lr}^18 .endif19 /**20 ARMv5T之前架构在ldm指令后需要一个nop21 *22 **/23 mov r0, r024 add sp, sp, #S_FRAME_SIZE - S_PC25 /**26 将spsr_svc赋值给cpsr并返回到用户态(见ARM ref manual)27 *28 **/29 movs pc, lr30 .endm
总结一下系统调用在内核中的流程, 跳转异常向量表, 保存用户态环境, 查表执行系统调用, 根据执行结果做不同处理, 恢复用户态环境并返回用户态.
让我们再次回到用户态, 之前讨论过errno是线程存储的, glibc通过跳转执行0xffff0fe0的指令来获取TLS数据段, 那么0xffff0fe0究竟存放了什么呢? 让我们来看下__kuser_get_tls()(defined in arch/arm/kernel/entry-armv.S), 该接口正好存放在该地址上(0xffff0000-0xffff1000是任何进程都会映射的地址, 因此访问该地址并不会触发异常), 该接口共占7条指令, 其中后4条初始化为0, 前三条指令分别将TLS地址保存在r0中, 跳转用户态, 硬件TLS指令, 反汇编指令见下. 除了跳转指令后没有必要再操作协处理器以外好像没什么问题.
1 .macro usr_ret, reg 2 #ifdef CONFIG_ARM_THUMB 3 bx \reg 4 #else 5 mov pc, \reg 6 #endif 7 .endm 8 __kuser_get_tls: @ 0xffff0fe0 9 ldr r0, [pc, #(16 - 8)] @ read TLS, set in kuser_get_tls_init10 usr_ret lr11 mrc p15, 0, r0, c13, c0, 3 @ 0xffff0fe8 hardware TLS code12 .rep 413 .word 0 @ 0xffff0ff0 software TLS value, then14 .endr @ pad up to __kuser_helper_version15 __kuser_helper_version: @ 0xffff0ffc16 .word ((__kuser_helper_end - __kuser_helper_start) >> 5)17 c053d9e0 <__kuser_get_tls>:18 c053d9e0: e59f0008 ldr r0, [pc, #8] ; c053d9f0 <__kuser_get_tls+0x10>19 c053d9e4: e12fff1e bx lr20 c053d9e8: ee1d0f70 mrc 15, 0, r0, cr13, cr0, { 3}21 ...22 c053d9fc <__kuser_helper_version>:23 c053d9fc: 00000005 andeq r0, r0, r5
幸好我又做了次试验! 不然又要打脸了. 打印结果显示前两条指令和反汇编内核结果不同, 其中0xe1a0f00e是mov pc, lr指令, 与bx lr类似先不讨论, 为什么0xfe0与0xfe8的指令相同?
1 int main() 2 { 3 unsigned int i = 0, *p = 0xffff0fe0; 4 for (i = 0; i < 8; i++) 5 printf("%p: 0x%x\n", p + i, *(p + i)); 6 } 7 #arm-hisiv400-linux-gcc test.c 8 # ./a.out? 9 0xffff0fe0: 0xee1d0f7010 0xffff0fe4: 0xe1a0f00e11 0xffff0fe8: 0xee1d0f7012 0xffff0fec: 0x013 0xffff0ff0: 0x014 0xffff0ff4: 0x015 0xffff0ff8: 0x016 0xffff0ffc: 0x5
看了下注释找到了kuser_get_tls_init()(defined in arch/arm/kernel/traps.c), 该函数在early_trap_init()(defined in arch/arm/kernel/traps.c)中被调用(即初始化异常向量表的函数). 其中tls_emu/has_tls_reg(defined in arch/arm/include/asm/tls.h)是架构相关宏, 分别定义了是否模拟TLS与是否使用TLS寄存器, 此处由于我们定义了CPU_32v6K, 因此不使用TLS模拟且支持TLS寄存器. 故系统初始化时会将0xfe8指令拷贝到0xfe0.
1 static void __init kuser_get_tls_init(unsigned long vectors)2 {3 if (tls_emu || has_tls_reg)4 memcpy((void *)vectors + 0xfe0, (void *)vectors + 0xfe8, 4);5 }
顺带一提是在定义了CPU_32v6K时设置TLS寄存器的操作set_tls(defined in arch/arm/include/asm/tls.h)正好与该指令相反. 该宏是在__switch_to(defined in arch/arm/kernel/entry-armv.S)汇编函数中调用的, 该函数我们在分析调度接口scheduled()时提到过, 是切换上下文的函数, 具体可见之前的分析.
1 .macro set_tls_v6k, tp, tmp1, tmp22 mcr p15, 0, \tp, c13, c0, 3 @ set TLS register3 mov \tmp1, #04 mcr p15, 0, \tmp1, c13, c0, 2 @ clear user r/w TLS register5 .endm6 #define set_tls set_tls_v6k
最后说下0xfe8处指令的作用, CP13是线程ID寄存器(for more detail see ARM architecture reference manual markup B4.6.35), 即专门用于存储线程相关信息的寄存器, 自ARMv7引入.
本来想写些关于线程跟踪实现的, 结果一懒又挖坑不填了(主要是glibc看起来太消耗精力了,比内核复杂一万倍), 添了点tls的内容滥竽充数, 剩下的以后再说吧.