查看原文
其他

鸿蒙内核源码分析(系统调用篇)

云天实验室 哆啦安全 2022-08-17

技术架构

OpenHarmony整体遵从分层设计,从下向上依次为:内核层、系统服务层、框架层和应用层。系统功能按照"系统 > 子系统 > 组件"逐级展开,在多设备部署场景下,支持根据实际需求裁剪某些非必要的组件。OpenHarmony技术架构,如下所示:




鸿蒙内核源码目录结构

kernel_liteos_a


鸿蒙内核源码(官方版)

https://gitee.com/openharmonyhttps://gitee.com/openharmony/kernel_liteos_ahttps://gitee.com/openharmony/kernel_liteos_m



鸿蒙内核源码分析(源码注释篇)

kernel_liteos_a_note

鸿蒙内核源码注释中文版 -> 点击目录和文件查看源码的详细中文注解

kernel_liteos_a_notekernelbasecorelos_bitmap.c # 位图管理器有什么作用? 在内核常应用于哪些场景?los_process.c # 鸿蒙内核源码分析(进程管理篇) -> 进程是内核的资源管理单元,它是如何管理任务、内存、文件的,进程间是如何协作的?los_sortlink.c # 排序链表的实现,它的应用场景是怎样的?los_swtmr.c # 内核的定时器是如何实现和管理的?los_sys.c # 几个跟tick相关的转化函数los_task.c # 鸿蒙内核源码分析(Task管理篇) -> Task是内核调度的单元,它解决了什么问题? 如何调度?los_tick.c # 鸿蒙内核源码分析(时钟管理篇) -> 是谁在一直触发调度? 硬时钟中断都干了些什么事?los_timeslice.c                  # 进程和任务能一直占有CPU吗? 怎么合理的分配时间?ipc # 进程间通讯有哪些方式? 请说出三种? 是如何实现的?los_event.c -> 事件解决了什么问题? 怎么管理的?los_futex.c # futex是Fast Userspace muTexes的缩写(快速用户空间互斥体),它有什么作用?los_ipcdebug.c                   # 进程间通讯如何调试?los_mux.c # 互斥量,有你没我的零和博弈, 为什么需要互斥量? 是如何实现的?los_queue.c # 内核消息队列是如何实现的? 对长度和大小有限制吗?los_queue_debug.c                # 如何调试消息队列?los_sem.c # 信号量解决了什么问题? 它的本质是什么?los_sem_debug.c                  # 如何调试信号量?los_signal.c                     # 信号解决了什么问题? 你知道哪些信号?mem ->misc -> kill_shellcmd.c          # shell命令kill的实现,熟悉的 kill 9 18的背后发生了什么?los_misc.c ->los_stackinfo.c                  # 栈有哪些信息? 如何检测栈是否异常?mempt_shellcmd.c # 和内存相关的shell命令有哪些?swtmr_shellcmd.c # 和软时钟相关的shell命令有哪些?sysinfo_shellcmd.c               # 和系统信息相关的shell命令有哪些?task_shellcmd.c                  # 和任务相关的shell命令有哪些?vm_shellcmd.c # 和虚拟内存相关的shell命令有哪些?mp -> los_lockdep.c              # 死锁是怎么发生的? 如何检测死锁?los_mp.c # 鸿蒙支持多CPU吗 ? 它们是如何工作的? CPU之间是如何通讯的?los_percpu.c # CPU有哪些信息?los_stat.c # CPU的运行信息如何统计?om -> los_err.c ->sched/sched_sq -> los_priqueue.c # 鸿蒙内核源码分析(调度队列篇) -> 为什么只有就绪状态才会有队列?los_sched.c # 鸿蒙内核源码分析(调度机制篇) -> 哪些情况下会触发调度? 调度算法是怎样的?vm # 鸿蒙内核源码分析(内存规则篇) -> 什么是虚拟内存? 虚拟内存全景图是怎样的?los_vm_boot.c -> -> 开机阶段内存是如何初始化的?los_vm_dump.c # 如何dump内存数据?los_vm_fault.c # 为什么会缺页? 缺页怎么处理?los_vm_filemap.c # 文件和内存是如何映射? 什么是写时拷贝技术(cow)?los_vm_iomap.c # 设备和内存是如何映射?los_vm_map.c # 鸿蒙内核源码分析(内存映射篇) -> 内核空间,用户空间,线性区是如何分配的,虚拟内存<-->物理内存是如何映射的?los_vm_page.c # 什么是物理页框,哪些地方会用到它?los_vm_phys.c # 鸿蒙内核源码分析(物理内存篇) -> 段页式管理,物理内存是如何分配和回收的?los_vm_scan.c # LRU算法是如何运作的?los_vm_syscall.c # 系统调用之内存, 用户进程如何申请内存? 底层发生了什么?oom.c # 内存溢出是如何检测的?shm.c # 共享内存是如何实现的?common -> console.c              # 熟悉的控制台是如何实现的?hwi_shell.c # 如何查询硬件中断?los_cir_buf.c # 环形缓冲区的读写是如何实现的 ? 常用于什么场景下?los_config.c # 内核有哪些配置信息?los_exc_interaction.c            # 任务出现异常如何检测?los_excinfo.c # 异常有哪些信息? 如何记录异常信息?los_hilog.c # 内核是如何封装日志的?los_magickey.c # 魔法键有什么作用?los_printf.c # 内核对 printf 做了哪些封装?los_rootfs.c # 什么是根文件系统? 为什么需要它?los_seq_buf.c ->virtual_serial.c ->extended -> cppsupport -> ->los_cppsupport.c ->cpup -> cpup_shellcmd.c ->los_cpup.c ->dynload/src ->los_exec_elf.c ->los_load_elf.c ->liteipc -> hm_liteipc.c ->tickless -> los_tickless.c ->trace -> los_trace.c -> ->vdso -> src -> los_vdso.c -> los_vdso_text.S ->usr -> los_vdso_sys.c ->user/src -> los_user_init.c ->



鸿蒙内核源码注释(中文注释版)

https://gitee.com/weharmony/kernel_liteos_a_notegit clone https://gitee.com/weharmony/kernel_liteos_a_note.git



通过一张图和七段代码详细说明系统调用的整个过程,逐步跟踪系统调用实现全过程

这里的模式可以理解为空间,因为模式不同运行的栈空间就不一样


过程解读

在应用层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.应用程序mainint main(void)

{ char mqname[NAMESIZE], msgrv1[BUFFER], msgrv2[BUFFER]; const char *msgptr1 = "test message1"; const char *msgptr2 = "test message2 with differnet length"; mqd_t mqdes; int prio1 = 1, prio2 = 2; struct timespec ts; struct mq_attr attr; int unresolved = 0, failure = 0;
sprintf(mqname, "/" FUNCTION "_" TEST "_%d", getpid()); attr.mq_msgsize = BUFFER; attr.mq_maxmsg = BUFFER; mqdes = mq_open(mqname, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr);
if (mqdes == (mqd_t)-1) { perror(ERROR_PREFIX "mq_open"); unresolved = 1; }
if (mq_send(mqdes, msgptr1, strlen(msgptr1), prio1) != 0) { perror(ERROR_PREFIX "mq_send"); unresolved = 1; }
printf("Test PASSED\n"); return PTS_PASS;}


2.mq_open发起系统调用mqd_t mq_open(const char *name, int flags, ...)

{ mode_t mode = 0; struct mq_attr *attr = 0;
if (*name == '/') name++;
if (flags & O_CREAT) { va_list ap; va_start(ap, flags); mode = va_arg(ap, mode_t); attr = va_arg(ap, struct mq_attr *); va_end(ap); }
return syscall(SYS_mq_open, name, flags, mode, attr);}
解读SYS_mq_open是真正的系统调用函数,对应一个系统调用号__NR_mq_open,通过宏SYSCALL_HAND_DEF将SysMqOpen注册到g_syscallHandle中.static UINTPTR g_syscallHandle[SYS_CALL_NUM] = {0}; //系统调用入口函数注册
static UINT8 g_syscallNArgs[(SYS_CALL_NUM + 1) / NARG_PER_BYTE] = {0};//保存系统调用对应的参数数量
#define SYSCALL_HAND_DEF(id, fun, rType, nArg)if ((id) { g_syscallHandle[(id)] = (UINTPTR)(fun); g_syscallNArgs[(id) / NARG_PER_BYTE] |= ((id) & 1) ? (nArg) <} \
#include "syscall_lookup.h"#undef SYSCALL_HAND_DEF
SYSCALL_HAND_DEF(__NR_mq_open, SysMqOpen, mqd_t, ARG_NUM_4)
g_syscallNArgs为注册函数的参数个数,也会一块记录下来.
四个参数为 SYS_mq_open的四个参数,后续将保存在R0~R3寄存器中


3.syscalllong syscall(long n, ...)

{ va_list ap; syscall_arg_t a,b,c,d,e,f;
va_start(ap, n); a=va_arg(ap, syscall_arg_t); b=va_arg(ap, syscall_arg_t); c=va_arg(ap, syscall_arg_t); d=va_arg(ap, syscall_arg_t); e=va_arg(ap, syscall_arg_t); f=va_arg(ap, syscall_arg_t);//最多6个参数 va_end(ap);
return __syscall_ret(__syscall(n,a,b,c,d,e,f));}
//4个参数的系统调用时底层处理
static inline long __syscall4(long n, long a, long b, long c, long d){ register long a7 __asm__("a7") = n; //将系统调用号保存在R7寄存器 register long a0 __asm__("a0") = a; //R0 register long a1 __asm__("a1") = b; //R1 register long a2 __asm__("a2") = c; //R2 register long a3 __asm__("a3") = d; //R3
__asm_syscall("r"(a7), "0"(a0), "r"(a1), "r"(a2), "r"(a3))}
解读可变参数实现所有系统调用的参数的管理,可以看出,在鸿蒙内核中系统调用的参数最多不能大于6个R7寄存器保存了系统调用号,R0~R5保存具体每个参数
可变参数的具体实现后续有其余篇幅详细介绍


4.svc 0//切到SVC模式

#define __asm_syscall(...) do { \ __asm__ __volatile__ ( "svc 0" \ : "=r"(x0) : __VA_ARGS__ : "memory", "cc"); \ return x0; \} while (0)
系统调用号存放在r7寄存器,参数存放在r0,r1,r2寄存器中,返回值最终会存放在寄存器r0中



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

解读svc全称是SuperVisor Call,完成工作模式的切换.不管之前是7个模式中的哪个模式,统一都切到SVC管理模式.但也许会好奇,ARM软中断不是用SWI吗,这里怎么变成了SVC了,请看下面一段话,是从ARM官网翻译的:

SVC超级用户调用。


语法

SVC{cond} #immed


其中:

cond 是一个可选的条件代码(请参阅条件执行)。

immed 是一个表达式,其取值为以下范围内的一个整数:


在ARM指令中为0到224–1(24位值)

在16位Thumb指令中为0-255(8位值)


用法

SVC指令会引发一个异常。这意味着处理器模式会更改为超级用户模式,CPSR会保存到超级用户模式SPSR,并且执行会跳转到SVC向量(请参阅《开发指南》中的第6章处理处理器异常)。

处理器会忽略immed。但异常处理程序会获取它,借以确定所请求的服务。


Note

作为ARM汇编语言开发成果的一部分,SWI指令已重命名为SVC。在此版本的RVCT中,SWI指令反汇编为SVC,并提供注释以指明这是以前的SWI。


条件标记

此指令不更改标记。

体系结构

此ARM 指令可用于所有版本的ARM体系结构。

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


5._osExceptSwiHdl@ Description: Software interrupt exception handler

_osExceptSwiHdl:@软中断异常处理@保存任务上下文(TaskContext)开始...一定要对照TaskContext来理解
SUB SP, SP, #(4 * 16)@先申请16个栈空间用于处理本次软中断STMIA SP, {R0-R12}@TaskContext.R[GEN_REGS_NUM] STMIA从左到右执行,先放R0 .. R12MRS R3, SPSR@读取本模式下的SPSR值MOV R4, LR@保存回跳寄存器LRAND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式CMP R1, #CPSR_USER_MODE @ User mode是否为用户模式BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转
@ 当为用户模式时,获取SP和LR寄出去值@ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr).@ stmia with ^ will return the user mode registers (provided that r15 is not in the register list).
MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 => TaskContext.regPSRADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PCSTMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 从右向左 保存 => TaskContext.LR和SPSUB SP, SP, #4 @ => TaskContext.resvedPUSH_FPU_REGS R1@保存中断模式(用户模式模式)
@保存任务上下文(TaskContext) 结束
MOV FP, #0 @ Init frame pointerCPSIE I@开中断,表明在系统调用期间可响应中断BLX OsArmA32SyscallHandle/*交给C语言处理系统调用,参数为R0,指向TaskContext的开始位置*/CPSID I@执行后续指令前必须先关中断
@恢复任务上下文(TaskContext) 开始
POP_FPU_REGS R1 @弹出FP值给R1ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值
@ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr).@ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list)
LDMFD SP!, {R0-R12} @恢复R0-R12寄存器LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行
@恢复任务上下文(TaskContext) 结束
OsKernelSVCHandler:@主要目的是保存ExcContext中除(R0~R12)的其他寄存器ADD R0, SP, #(4 * 16)@跳转到保存PC,LR,SP的位置,此时R0位置刚好是SP的位置MOV R5, R0@由R5记录SP位置,因为R0要暂时充当SP寄存器来使用STMFD R0!, {R4} @ Store PC => ExcContext.PCSTMFD R0!, {R4} @ 相当于保存了=> ExcContext.LRSTMFD R0!, {R5} @ 相当于保存了=> ExcContext.SPSTMFD SP!, {R3} @ Push task`s CPSR (i.e. exception SPSR). =>ExcContext.regPSRSUB SP, SP, #(4 * 2) @ user sp and lr => =>ExcContext.USP,ULRMOV R0, #OS_EXCEPT_SWI @ Set exception ID to OS_EXCEPT_SWI.
@ 设置异常ID为软中断
B _osExceptionSwi @ Branch to global exception handler.
@ 跳到全局异常处理


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

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

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

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

注意看OsArmA32SyscallHandle的参数UINT32 *regs


6.OsArmA32SyscallHandle/* The SYSCALL ID is in R7 on entry.  Parameters follow in R0..R6 */

/******************************************************************由汇编调用,见于 los_hw_exc.s / BLX OsArmA32SyscallHandleSYSCALL是产生系统调用时触发的信号,R7寄存器存放具体的系统调用ID,也叫系统调用号regs:参数就是所有寄存器注意:本函数在用户态和内核态下都可能被调用到//MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数******************************************************************/
LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) { UINT32 ret; UINT8 nArgs; UINTPTR handle; UINT32 cmd = regs[REG_R7];//C7寄存器记录了触发了具体哪个系统调用
if (cmd >= SYS_CALL_NUM) {//系统调用的总数 PRINT_ERR("Syscall ID: error %d !!!\n", cmd); return regs; }
if (cmd == __NR_sigreturn) {//收到 __NR_sigreturn 信号 OsRestorSignalContext(regs);//恢复信号上下文 return regs; }
handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,类似 SysRead nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数
if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个 PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); regs[REG_R0] = -ENOSYS; return regs; }
//regs[0-6] 记录系统调用的参数,这也是由R7寄存器保存系统调用号的原因 switch (nArgs) {//参数的个数 case ARG_NUM_0: case ARG_NUM_1:
ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname); break;
case ARG_NUM_2://如何是两个参数的系统调用,这里传三个参数也没有问题,因被调用函数不会去取用R2值 case ARG_NUM_3: ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp); break;
case ARG_NUM_4: case ARG_NUM_5: ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4]); break;
default://7个参数的情况 ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4], regs[REG_R5], regs[REG_R6]); }
regs[REG_R0] = ret;//R0保存系统调用返回值 OsSaveSignalContext(regs);//保存信号上下文现场
/* Return the last value of curent_regs. This supports context switches on return from the exception. * That capability is only used with theSYS_context_switch system call. */
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.SysMqOpenmqd_t SysMqOpen(const char *mqName, int openFlag, mode_t mode, struct mq_attr *attr)

{ mqd_t ret; int retValue; char kMqName[PATH_MAX + 1] = { 0 }; retValue = LOS_StrncpyFromUser(kMqName, mqName, PATH_MAX); if (retValue return retValue;}
ret = mq_open(kMqName, openFlag, mode, attr);//一个消息队列可以有多个进程向它读写消息
if (ret == -1) { return (mqd_t)-get_errno();
}
return ret;}


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

SysMqOpen是返回到
OsArmA32SyscallHandle regs[REG_R0] = ret;
OsArmA32SyscallHandle再返回到_osExceptSwiHdl_osExceptSwiHdl后面的代码是用于恢复用户模式现场和SPSR、PC等寄存器

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


https://gitee.com/LiteOS/https://gitee.com/LiteOS/LiteOS_Studio/releases/
https://gitee.com/openharmony/kernel_liteos_ahttps://gitee.com/openharmony/kernel_liteos_m
https://gitee.com/weharmony/kernel_liteos_a_note


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存