鸿蒙内核源码分析(系统调用篇) | 图解系统调用全貌

系统
本篇通过一张图和七段代码详细说明系统调用的整个过程,代码一捅到底,直到汇编层再也捅不下去. 先看图,这里的模式可以理解为空间,因为模式不同运行的栈空间就不一样。

[[387734]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

本篇说清楚系统调用

读本篇之前建议先读鸿蒙内核源码分析(总目录)工作模式篇.

本篇通过一张图和七段代码详细说明系统调用的整个过程,代码一捅到底,直到汇编层再也捅不下去. 先看图,这里的模式可以理解为空间,因为模式不同运行的栈空间就不一样.

过程解读

● 在应用层main中使用系统调用mq_open(posix标准接口)

● mq_open被封装在库中,这里直接看库里的代码.

● mq_open中调用syscall,将参数传给寄出器 R7,R0~R6

● SVC 0 完成用户模式到内核模式(SVC)的切换

● _osExceptSwiHdl运行在svc模式下.

● PC寄存器直接指向_osExceptSwiHdl处取指令.

● _osExceptSwiHdl是汇编代码,先保存用户模式现场(R0~R12寄存器),并调用OsArmA32SyscallHandle完成系统调用

● OsArmA32SyscallHandle中通过系统调用号(保存在R7寄存器)查询对应的注册函数SYS_mq_open

● SYS_mq_open是本次系统调用的实现函数,完成后return回到OsArmA32SyscallHandle

● OsArmA32SyscallHandle再return回到_osExceptSwiHdl

● _osExceptSwiHdl恢复用户模式现场(R0~R12寄存器)

● 从内核模式(SVC)切回到用户模式,PC寄存器也切回用户现场.

● 由此完成整个系统调用全过程

七段追踪代码,逐个分析

1.应用程序 main

  1. int main(void) 
  2.     char mqname[NAMESIZE], msgrv1[BUFFER], msgrv2[BUFFER]; 
  3.     const char *msgptr1 = "test message1"
  4.     const char *msgptr2 = "test message2 with differnet length"
  5.     mqd_t mqdes; 
  6.     int prio1 = 1, prio2 = 2; 
  7.     struct timespec ts; 
  8.     struct mq_attr attr; 
  9.     int unresolved = 0, failure = 0; 
  10.     sprintf(mqname, "/" FUNCTION "_" TEST "_%d", getpid()); 
  11.     attr.mq_msgsize = BUFFER; 
  12.     attr.mq_maxmsg = BUFFER; 
  13.     mqdes = mq_open(mqname, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr); 
  14.     if (mqdes == (mqd_t)-1) { 
  15.         perror(ERROR_PREFIX "mq_open"); 
  16.         unresolved = 1; 
  17.     } 
  18.     if (mq_send(mqdes, msgptr1, strlen(msgptr1), prio1) != 0) { 
  19.         perror(ERROR_PREFIX "mq_send"); 
  20.         unresolved = 1; 
  21.     } 
  22.     printf("Test PASSED\n"); 
  23.     return PTS_PASS; 

2. mq_open 发起系统调用

  1. mqd_t mq_open(const char *nameint flags, ...) 
  2.     mode_t mode = 0; 
  3.     struct mq_attr *attr = 0; 
  4.     if (*name == '/'name++; 
  5.     if (flags & O_CREAT) { 
  6.         va_list ap; 
  7.         va_start(ap, flags); 
  8.         mode = va_arg(ap, mode_t); 
  9.         attr = va_arg(ap, struct mq_attr *); 
  10.         va_end(ap); 
  11.     } 
  12.     return syscall(SYS_mq_open, name, flags, mode, attr); 

解读

● SYS_mq_open 是真正的系统调用函数,对应一个系统调用号__NR_mq_open,通过宏SYSCALL_HAND_DEF将SysMqOpen注册到g_syscallHandle中.

  1. static UINTPTR g_syscallHandle[SYS_CALL_NUM] = {0}; //系统调用入口函数注册 
  2. static UINT8 g_syscallNArgs[(SYS_CALL_NUM + 1) / NARG_PER_BYTE] = {0};//保存系统调用对应的参数数量 
  3. #define SYSCALL_HAND_DEF(id, fun, rType, nArg)                                             \ 
  4.     if ((id) < SYS_CALL_NUM) {                                                             \ 
  5.         g_syscallHandle[(id)] = (UINTPTR)(fun);                                            \ 
  6.         g_syscallNArgs[(id) / NARG_PER_BYTE] |= ((id) & 1) ? (nArg) << NARG_BITS : (nArg); \ 
  7.     }                                                                                      \ 
  8.  
  9.     #include "syscall_lookup.h" 
  10. #undef SYSCALL_HAND_DEF 
  11.  
  12. SYSCALL_HAND_DEF(__NR_mq_open, SysMqOpen, mqd_t, ARG_NUM_4)   

● g_syscallNArgs为注册函数的参数个数,也会一块记录下来.

● 四个参数为 SYS_mq_open的四个参数,将保存在R0~R3寄存器中

3. syscall

  1. long syscall(long n, ...) 
  2.     va_list ap; 
  3.     syscall_arg_t a,b,c,d,e,f; 
  4.     va_start(ap, n); 
  5.     a=va_arg(ap, syscall_arg_t); 
  6.     b=va_arg(ap, syscall_arg_t); 
  7.     c=va_arg(ap, syscall_arg_t); 
  8.     d=va_arg(ap, syscall_arg_t); 
  9.     e=va_arg(ap, syscall_arg_t); 
  10.     f=va_arg(ap, syscall_arg_t);//最多6个参数 
  11.     va_end(ap); 
  12.     return __syscall_ret(__syscall(n,a,b,c,d,e,f)); 
  13. static inline long __syscall4(long n, long a, long b, long c, long d) 
  14.     register long a7 __asm__("a7") = n; //系统调用号 R7寄存器 
  15.     register long a0 __asm__("a0") = a; //R0 
  16.     register long a1 __asm__("a1") = b; //R1 
  17.     register long a2 __asm__("a2") = c; //R2 
  18.     register long a3 __asm__("a3") = d; //R3 
  19.     __asm_syscall("r"(a7), "0"(a0), "r"(a1), "r"(a2), "r"(a3)) 

解读

● 可变参数实现所有系统调用的参数的管理,可以看出,在鸿蒙内核中系统调用的参数最多不能大于6个

● R7寄存器保存了系统调用号,R0~R5保存具体每个参数

● 可变参数的具体实现后续有其余篇幅详细介绍,敬请关注.

4. svc 0

  1. //切到SVC模式 
  2. #define __asm_syscall(...) do { \ 
  3.     __asm__ __volatile__ ( "svc 0" \ 
  4.     : "=r"(x0) : __VA_ARGS__ : "memory""cc"); \ 
  5.     return x0; \ 
  6.     } while (0) 

  1. b   reset_vector            @开机代码 
  2.  b   _osExceptUndefInstrHdl     @异常处理之CPU碰到不认识的指令 
  3.  b   _osExceptSwiHdl            @异常处理之:软中断 
  4.  b   _osExceptPrefetchAbortHdl  @异常处理之:取指异常 
  5.  b   _osExceptDataAbortHdl      @异常处理之:数据异常 
  6.  b   _osExceptAddrAbortHdl      @异常处理之:地址异常 
  7.  b   OsIrqHandler               @异常处理之:硬中断 
  8.  b   _osExceptFiqHdl                @异常处理之:快中断 

解读

● svc 全称是 SuperVisor Call,完成工作模式的切换.不管之前是7个模式中的哪个模式,统一都切到SVC管理模式

● 而软中断对应的处理函数为 _osExceptSwiHdl,即PC寄存器将跳到_osExceptSwiHdl执行

5. _osExceptSwiHdl

  1. @ Description: Software interrupt exception handler 
  2. _osExceptSwiHdl: @软中断异常处理 
  3.     SUB     SP, SP, #(4 * 16)   @先申请16个栈空间用于处理本次软中断 
  4.     STMIA   SP, {R0-R12}        @保存R0-R12寄存器值 
  5.     MRS     R3, SPSR            @读取本模式下的SPSR值 
  6.     MOV     R4, LR              @保存回跳寄存器LR 
  7.  
  8.     AND     R1, R3, #CPSR_MASK_MODE                          @ Interrupted mode 获取中断模式 
  9.     CMP     R1, #CPSR_USER_MODE                              @ User mode    是否为用户模式 
  10.     BNE     OsKernelSVCHandler                               @ Branch if not user mode 非用户模式下跳转 
  11.     @ 当为用户模式时,获取SP和LR寄出去值 
  12.     @ we enter from user mode, we need get the values of  USER mode r13(sp) and r14(lr). 
  13.     @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). 
  14.     MOV     R0, SP                                           @获取SP值,R0将作为OsArmA32SyscallHandle的参数 
  15.     STMFD   SP!, {R3}                                        @ Save the CPSR 入栈保存CPSR值 
  16.     ADD     R3, SP, #(4 * 17)                                @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 
  17.     STMFD   R3!, {R4}                                        @ Save the CPSR and r15(pc) 保存LR寄存器 
  18.     STMFD   R3, {R13, R14}^                                  @ Save user mode r13(sp) and r14(lr) 保存用户模式下的SP和LR寄存器 
  19.     SUB     SP, SP, #4 
  20.     PUSH_FPU_REGS R1    @保存中断模式(用户模式模式)                                          
  21.     MOV     FP, #0                                           @ Init frame pointer 
  22.     CPSIE   I   @开中断,表明在系统调用期间可响应中断 
  23.     BLX     OsArmA32SyscallHandle   /*交给C语言处理系统调用*/ 
  24.     CPSID   I   @执行后续指令前必须先关中断 
  25.  
  26.     POP_FPU_REGS R1                                          @弹出FP值给R1 
  27.     ADD     SP, SP,#4                                        @ 定位到保存旧SPSR值的位置 
  28.     LDMFD   SP!, {R3}                                        @ Fetch the return SPSR 弹出旧SPSR值 
  29.     MSR     SPSR_cxsf, R3                                    @ Set the return mode SPSR 恢复该模式下的SPSR值 
  30.  
  31.     @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). 
  32.     @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) 
  33.  
  34.     LDMFD   SP!, {R0-R12}                                    @恢复R0-R12寄存器 
  35.     LDMFD   SP, {R13, R14}^                                  @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 
  36.     ADD     SP, SP, #(2 * 4)                                 @定位到保存旧PC值的位置 
  37.     LDMFD   SP!, {PC}^                                       @ Return to user 切回用户模式运行 

解读

● 运行到此处,已经切到SVC的栈运行,所以先保存上一个模式的现场

● 获取中断模式,软中断的来源可不一定是用户模式,完全有可能是SVC本身,比如系统调用中又发生系统调用.就变成了从SVC模式切到SVC的模式

● MOV R0, SP ;sp将作为参数传递给OsArmA32SyscallHandle

● 调用 OsArmA32SyscallHandle 这是所有系统调用的统一入口

● 注意看OsArmA32SyscallHandle的参数 UINT32 *regs

6. OsArmA32SyscallHandle

  1. LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) 
  2.     UINT32 ret; 
  3.     UINT8 nArgs; 
  4.     UINTPTR handle; 
  5.     UINT32 cmd = regs[REG_R7];// R7寄存器记录了系统调用号 
  6.      
  7.     if (cmd >= SYS_CALL_NUM) {//系统调用的总数 
  8.         PRINT_ERR("Syscall ID: error %d !!!\n", cmd); 
  9.         return regs; 
  10.     } 
  11.  
  12.     if (cmd == __NR_sigreturn) {//此时运行在内核栈,程序返回的调用,从内核态返回用户态时触发 
  13.         OsRestorSignalContext(regs);//恢复信号上下文,执行完函数后,切到了用户栈 
  14.         return regs; 
  15.     } 
  16.  
  17.     handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,即 SYS_mq_open  
  18.     nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ 
  19.     nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数 
  20.     if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个 
  21.         PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); 
  22.         regs[REG_R0] = -ENOSYS; 
  23.         return regs; 
  24.     } 
  25.     //regs[0-6] 记录系统调用的参数,这也是由 R7 寄存器保存系统调用号的原因 
  26.     switch (nArgs) {//参数的个数  
  27.         case ARG_NUM_0: 
  28.         case ARG_NUM_1: 
  29.             ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname); 
  30.             break; 
  31.         case ARG_NUM_2://@note_thinking 如何是两个参数的系统调用,这里传的确是三个参数,任务栈中会出现怎样的情况呢? 
  32.         case ARG_NUM_3: 
  33.             ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp); 
  34.             break; 
  35.         case ARG_NUM_4: 
  36.         case ARG_NUM_5: 
  37.             ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], 
  38.                                          regs[REG_R4]); 
  39.             break; 
  40.         default:    //7个参数的情况 
  41.             ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], 
  42.                                          regs[REG_R4], regs[REG_R5], regs[REG_R6]); 
  43.     } 
  44.  
  45.     regs[REG_R0] = ret;//R0保存系统调用返回值 
  46.     OsSaveSignalContext(regs);//保存用户栈现场 
  47.  
  48.     /* Return the last value of curent_regs.  This supports context switches on return from the exception. 
  49.      * That capability is only used with theSYS_context_switch system call. 
  50.      */ 
  51.     return regs;//返回寄存器的值 

解读

● 参数是regs对应的就是R0~Rn

● R7保存的是系统调用号,R0~R3保存的是 SysMqOpen的四个参数

● g_syscallHandle[cmd]就能查询到 SYSCALL_HAND_DEF(__NR_mq_open, SysMqOpen, mqd_t, ARG_NUM_4)注册时对应的 SysMqOpen函数

● *(SyscallFun5)handle此时就是SysMqOpen

● 注意看 SysMqOpen 的参数是最开始的 main函数中的 mqdes = mq_open(mqname, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr); 由此完成了真正系统调用的过程

7. SysMqOpen

  1. mqd_t SysMqOpen(const char *mqName, int openFlag, mode_t mode, struct mq_attr *attr) 
  2.     mqd_t ret; 
  3.     int retValue; 
  4.     char kMqName[PATH_MAX + 1] = { 0 }; 
  5.  
  6.     retValue = LOS_StrncpyFromUser(kMqName, mqName, PATH_MAX); 
  7.     if (retValue < 0) { 
  8.         return retValue; 
  9.     } 
  10.     ret = mq_open(kMqName, openFlag, mode, attr);//一个消息队列可以有多个进程向它读写消息 
  11.     if (ret == -1) { 
  12.         return (mqd_t)-get_errno(); 
  13.     } 
  14.     return ret; 

解读

● 此处的mq_open和main函数的mq_open其实是两个函数体实现.一个是给应用层的调用,一个是内核层使用,只是名字一样而已.

● SysMqOpen是返回到 OsArmA32SyscallHandle regs[REG_R0] = ret;

● OsArmA32SyscallHandle再返回到 _osExceptSwiHdl

● _osExceptSwiHdl后面的代码是用于恢复用户模式现场和SPSR,PC 等寄存器.

以上为鸿蒙系统调用的整个过程.

关于寄存器(R0~R15)在每种模式下的使用方式,寄存器篇中已详细说明,请前往查看.

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-03-11 11:14:39

鸿蒙HarmonyOS应用

2021-06-03 08:03:13

网络

2021-04-08 09:32:17

鸿蒙HarmonyOS应用

2021-10-25 09:53:52

鸿蒙HarmonyOS应用

2021-04-02 09:42:54

鸿蒙HarmonyOS应用

2021-01-22 09:47:22

鸿蒙HarmonyOS应用开发

2021-04-09 16:39:41

鸿蒙HarmonyOS应用

2023-02-01 08:11:40

系统调用函数

2010-04-29 10:15:01

Unix系统

2022-01-14 08:39:47

鸿蒙HarmonyOS应用

2024-09-13 08:18:10

2022-08-02 09:15:32

系统网络前端

2021-01-29 13:29:53

系统调用

2011-01-11 16:11:03

2021-03-11 16:07:40

鸿蒙HarmonyOS应用开发

2021-03-24 17:18:41

鸿蒙HarmonyOS应用开发

2021-04-01 09:38:02

鸿蒙HarmonyOS应用

2023-11-17 08:02:34

系统调用linux

2023-02-10 08:11:43

Linux系统调用

2016-11-01 13:11:28

点赞
收藏

51CTO技术栈公众号