之前分析errno的实现时有讲过系统调用的实现, 但是写到一半烂尾了, 于是决定重新挖个坑(- -!).

假设我们调用了一个open(), 从pc指向open()入口到pc执行open()的后一条指令中间究竟发生了什么. 首先明确第一点, 当我们调用open()时并不是直接调用系统调用open, 而是调用glibc的封装函数open(). 让我们从头开始一步一步分析.
让我们来看下open()的声明, include/fcntl.h中并未声明该函数, 但它包含了io/fcntl.h, 而后者声明了该函数.

1 #ifndef __USE_FILE_OFFSET64
2 extern int open(const char *__file, int __oflag, ...) __nonnull((1));
3 #else /* ! __USE_FILE_OFFSET64 */
4 #ifdef __REDIRECT
5 extern int __REDIRECT(open, (const char *__file, int __oflag, ...), open64) __nonnull((1));
6 #else /* ! __REDIRECT */
7 #define open open64
8 #endif
9 #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_RAW
19 #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_RAW
33 #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 #endif
45 #undef INTERNAL_SYSCALL
46 #define INTERNAL_SYSCALL(name, err, nr, args...) \
47     INTERNAL_SYSCALL_RAW(SYS_ify(name), err, nr, args)
48 #undef INLINE_SYSCALL
49 #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_SYSCALL
2 #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>';

来看下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_ERRVAL
10 T_PSEUDO_END_ERRVAL(SYSCALL_SYMBOL)
11 #else
12 //常见的系统调用, 如果有错误码, 返回-1并设置errno
13 T_PSEUDO(SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
14 ret
15 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_0
10 #undef DOARGS_2
11 #define DOARGS_2 DOARGS_0
12 #undef DOARGS_3
13 #define DOARGS_3 DOARGS_0
14 #undef DOARGS_4
15 #define DOARGS_4 DOARGS_0
16 #undef DOARGS_5
17 #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_6
26 #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_7
37 #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_CALL
49 #define DO_CALL(syscall_name, args) \
50     DOARGS_##args; \
51     ldr r7, =SYS_ify (syscall_name); \
52     swi 0x0; \
53     UNDOARGS_##args
54 #undef PSEUDO
55 #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 ret
65 #define ret PSEUDO_RET
66 #undef PSEUDO_END
67 #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, $0
3 #define __syscall_error __syscall_error_1
4 #include <sysdeps/unix/arm/sysdep.S>

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, 1f
5 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     .endif
16 #endif

最后来看下反汇编, 印证我们的分析(其实是对着反汇编才看懂代码的).

 1 000be680 <__open>:
 2 be680:   e51fc028    ldr ip, [pc, #-40] ; be660 <mkdirat+0x150>
 3 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 ; 0x1000
11 be6a4:   312fff1e    bxcc lr
12 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, r0
16 be6b8:   e8bd000f    pop {r0, r1, r2, r3}
17 be6bc:   e3a07005    mov r7, #5
18 be6c0:   ef000000    svc 0x00000000
19 be6c4:   e1a07000    mov r7, r0
20 be6c8:   e1a0000c    mov r0, ip
21 be6cc:   eb007a41    bl dcfd8 <__libc_disable_asynccancel>
22 be6d0:   e1a00007    mov r0, r7
23 be6d4:   e49de004    pop {lr} ; (ldr lr, [sp], #4)
24 be6d8:   e49d7004    pop {r7} ; (ldr r7, [sp], #4)
25 be6dc:   e3700a01    cmn r0, #4096 ; 0x1000
26 be6e0:   312fff1e    bxcc lr
27 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, #0
32 00017f24 <__syscall_error_1>:
33 17f24:   e1a0c00e    mov ip, lr
34 17f28:   e1a01000    mov r1, r0
35 17f2c:   e3e00a0f    mvn r0, #61440 ; 0xf000
36 17f30:   e1a0e00f    mov lr, pc
37 17f34:   e240f01f    sub pc, r0, #31
38 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, #0
42 17f48:   e12fff1c    bx ip
43 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_table
100 #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, #0xff000000
108     eorne scno, r10, #__NR_OABI_SYSCALL_BASE
109     ldrne tbl, =sys_oabi_call_table
110 #elif !defined(CONFIG_AEABI)
111     bic scno, scno, #0xff000000
112     eor scno, scno, #__NR_SYSCALL_BASE
113 #endif
114 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_WORK
122     bne __sys_trace
123     /**
124       设置lr为ret_fast_syscall, 设置pc为系统调用入口
125       注意此处使用ldrcc(carry bit is clear), 若scno大于NR_syscalls则不会执行该指令
126       若pc被设置为系统调用入口则执行系统调用, 返回时执行ret_fast_syscall
127       只有非公共系统调用才会执行后面的代码
128      *
129     **/
130     cmp scno, #NR_syscalls
131     adr lr, BSYM(ret_fast_syscall)
132     ldrcc pc, [tbl, scno, lsl #2]
133     add r1, sp, #S_OFF
134 2:
135     /**
136       处理非常规(arm私有)与非法(未实现)系统调用
137       why(defined in arch/arm/kernel/entry-header.S)为r8
138       比较scno与arm私有系统调用基址(__ARM_NR_BASE), 大于走arm_syscall否则走sys_ni_syscall
139       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, #0
145     cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
146     eor r0, scno, #__NR_SYSCALL_BASE
147     bcs arm_syscall?
148     b sys_ni_syscall
149 #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, #4
158     str lr, [sp, #S_PC]
159     b ret_fast_syscall
160 #endif
161 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传递给scno
11       r0为syscall_trace_enter返回值, 为保存的scno或-1(系统调用号检查失败或信号挂起)
12       之后流程与vector_swi一致, 即压栈r0-r6, r4与r5, 然后查表并调用系统调用
13       如果scno大于NR_syscalls, 判断scno是否为-1, 是则调用ret_slow_syscall
14       否则为未实现的系统调用, 走入2b(见上文)
15      *
16     **/
17     adr lr, BSYM(__sys_trace_return)
18     mov scno, r0
19     add r1, sp, #S_R0 + S_OFF
20     cmp scno, #NR_syscalls
21     ldmccia r1, {r0 - r6}
22     stmccia sp, {r4, r5}
23     ldrcc pc, [tbl, scno, lsl #2]
24     cmp scno, #-1
25     bne 2b
26     add sp, sp, #S_OFF
27     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 ABI
12 #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, sp
14     mov r2, why
15     bl do_work_pending
16     cmp r0, #0
17     beq no_work_pending
18     movlt scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
19     ldmia sp, {r0 - r6}
20     b local_restart
21 ret_fast_syscall:
22     UNWIND(.fnstart)
23     UNWIND(.cantunwind)
24     disable_irq
25     /**
26       如果thread_info->flags需要调度或信号挂起或需信号处理则走入fast_work_pending
27       否则直接恢复用户态环境
28      *
29     **/
30     ldr r1, [tsk, #TI_FLAGS]
31     tst r1, #_TIF_WORK_MASK
32     bne fast_work_pending
33     asm_trace_hardirqs_on
34     /**
35       arch_ret_to_user()是架构相关代码
36       ct_user_exit()是与ct_user_exit()成对调用的上下文跟踪的代码
37      *
38     **/
39     arch_ret_to_user r1, lr
40     ct_user_enter
41     restore_user_regs fast = 1, offset = S_OFF
42     UNWIND(.fnend)
43 ret_slow_syscall:
44     disable_irq
45     ldr r1, [tsk, #TI_FLAGS]
46     tst r1, #_TIF_WORK_MASK
47     bne work_pending
48 no_work_pending:
49     asm_trace_hardirqs_on
50     arch_ret_to_user r1, lr
51     ct_user_enter save = 0
52     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 #endif
10     /**
11       fast path与slow path区别在于fast path不恢复r0(返回系统调用结果)
12      *
13     **/
14     .if \fast
15     ldmdb sp, {r1 - lr}^
16     .else
17     ldmdb sp, {r0 - lr}^
18     .endif
19     /**
20       ARMv5T之前架构在ldm指令后需要一个nop
21      *
22     **/
23     mov r0, r0
24     add sp, sp, #S_FRAME_SIZE - S_PC
25     /**
26       将spsr_svc赋值给cpsr并返回到用户态(见ARM ref manual)
27      *
28     **/
29     movs pc, lr
30 .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_init
10     usr_ret lr
11     mrc p15, 0, r0, c13, c0, 3 @ 0xffff0fe8 hardware TLS code
12     .rep 4
13     .word 0 @ 0xffff0ff0 software TLS value, then
14     .endr @ pad up to __kuser_helper_version
15 __kuser_helper_version: @ 0xffff0ffc
16     .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 lr
20 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: 0xee1d0f70
10 0xffff0fe4: 0xe1a0f00e
11 0xffff0fe8: 0xee1d0f70
12 0xffff0fec: 0x0
13 0xffff0ff0: 0x0
14 0xffff0ff4: 0x0
15 0xffff0ff8: 0x0
16 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, tmp2
2     mcr p15, 0, \tp, c13, c0, 3 @ set TLS register
3     mov \tmp1, #0
4     mcr p15, 0, \tmp1, c13, c0, 2 @ clear user r/w TLS register
5 .endm
6 #define set_tls set_tls_v6k

最后说下0xfe8处指令的作用, CP13是线程ID寄存器(for more detail see ARM architecture reference manual markup B4.6.35), 即专门用于存储线程相关信息的寄存器, 自ARMv7引入.

本来想写些关于线程跟踪实现的, 结果一懒又挖坑不填了(主要是glibc看起来太消耗精力了,比内核复杂一万倍), 添了点tls的内容滥竽充数, 剩下的以后再说吧.

转载于:https://www.cnblogs.com/Five100Miles/p/8878080.html

系统调用的实现(与errno的设置)相关推荐

  1. Linux ptrace系统调用详解:利用 ptrace 设置硬件断点

    <GDB调试之ptrace实现原理> <C语言程序调用栈:backtrace+backtrace_symbols+backtrace_symbols_fd> <strac ...

  2. Linux内核分析——第五章 系统调用

    第五章 系统调用 5.1 与内核通信 1.系统调用在用户空间进程和硬件设备之间添加了一个中间层,该层主要作用有三个: (1)为用户空间提供了一种硬件的抽象接口 (2)系统调用保证了系统的稳定和安全 ( ...

  3. Linux系统调用的实现机制分析

    [摘要]本文介绍了系统调用的一些实现细节.首先分析了系统调用的意义,它们与库函数和应用程序接口有怎样的关系.然后,我们考察了内核如何实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和 ...

  4. 慢系统调用与信号中断

    http://blog.csdn.net/dianhuiren/article/details/7291540(博客不错) 早期的Unix系统,如果进程在一个'慢'系统调用中阻塞时,捕获到一个信号,这 ...

  5. Linux系统调用的运行过程【转】

    本文转自:http://blog.csdn.net/kernel_learner/article/details/7331505 在Linux中,系统调用是用户空间访问内核的唯一手段,它们是内核唯一的 ...

  6. ie传递给系统调用的数据区域太小_【Linux系列】系统调用

    在现代OS中,内核提供了用户进程与内核进行交互的一组接口.这些接口让应用程序受限地访问硬件设备,提供了创建新进程并与已有进程进行通信的机制,也提供了申请OS其他资源的能力. 系统调用在用户空间进程和硬 ...

  7. Linux系统调用相关概念

    目录: 1. Linux系统调用原理 2. 系统调用的实现 3. Linux系统调用分类及列表 4.系统调用.用户编程接口(API).系统命令和内核函数的关系 5. Linux系统调用实例 6. Li ...

  8. c语言网络编程阻塞,c语言网络编程-设置非阻塞方式

    #include #include #include #include #include #include #include #include #include #include #define MA ...

  9. Linux系统调用过程分析

    参考: <Linux内核设计与实现> 0 摘要 linux的系统调用过程: 层次如下: 用户程序------>C库(即API):INT 0x80 ----->system_ca ...

最新文章

  1. too many levels of symbolic links的错误
  2. vb.net中report不显示中文_Ubuntu中vi文本编辑器的使用
  3. 从PHP安全讲DedeCms的安全加固
  4. 烧水壶起水沟了怎么办?
  5. POJ2352 stars(树状数组)
  6. MS提供的Oracle for ADO 驱动,在一个应用中不能打开同时多个数据库连接
  7. Java注解 编译_Java注解(3)-注解处理器(编译期|RetentionPolicy.SOURCE)
  8. 现代软件工程个人作业进度
  9. 从内容/用户画像到如何做算法研发
  10. Mysql之数据库与sql
  11. python 视频转图片和图片转视频
  12. B/S神思SS628(100)身份证阅读器开发
  13. Spyder5 启动报错 spyder-kernels
  14. 德莱联盟 计算几何 线段相交
  15. 操作系统笔记(1.5w字耐心整理)
  16. JVM底层原理之标配参数、X和XX参数
  17. 苏州大学2021年全日制博士学位研究生招生简章
  18. 用matlab画一些骚东西,求助matlab大神,学校的课程安排太骚了,我们压根就不用学matlab...
  19. 美团点评高级1234面:算法+HashMap+Zookeeper+线程+Redis+kafka
  20. 算法:深度优先搜索(一)

热门文章

  1. linux启动keepalived服务,keepalived的原理及安装应用
  2. php5.3二进制包,php使用pack处理二进制文件的方法
  3. python apply_async数据量大不执行_apply_async里面的函数不执行
  4. centos nginx不是命令_虚拟机下Centos 8.0 安装PHP+Mysql+Nginx
  5. matlab图像处理函数
  6. 【GAN优化】解决模式崩溃的两条思路:改进优化和网络架构
  7. openresty开发系列17--lua中的正则表达式
  8. rpm批量卸载所有带有Java的文件
  9. 什么样的鼠标对程序员最有用,超级提高开发效率
  10. Jenkins系统上的时间不正确问题