Skip to content

95_系统调用篇

kuangyufei edited this page Sep 5, 2022 · 1 revision

本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

编译运行相关篇为:

本篇说清楚系统调用

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

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

过程解读

  • 在应用层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

int 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 = 1prio2 = 2;
	struct timespec ts;
	struct mq_attr attr;
	int unresolved = 0failure = 0;
	sprintf(mqname, "/" FUNCTION "_" TEST "_%d", getpid());
	attr.mq_msgsize = BUFFER;
	attr.mq_maxmsg = BUFFER;
	mqdes = mq_open(mqnameO_CREAT | O_RDWRS_IRUSR | S_IWUSR&attr);
	if (mqdes == (mqd_t)-1) {
		perror(ERROR_PREFIX "mq_open");
		unresolved = 1;
	}
	if (mq_send(mqdesmsgptr1strlen(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 *nameint flags, ...)
{
	mode_t mode = 0;
	struct mq_attr *attr = 0;
	if (*name == '/') name++;
	if (flags & O_CREAT) {
		va_list ap;
		va_start(apflags);
		mode = va_arg(apmode_t);
		attr = va_arg(apstruct mq_attr *);
		va_end(ap);
	}
	return syscall(SYS_mq_opennameflagsmodeattr);
}

解读

  • 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(idfunrTypenArg)                                             \
    if ((id) < SYS_CALL_NUM) {                                                             \
        g_syscallHandle[(id)] = (UINTPTR)(fun);                                            \
        g_syscallNArgs[(id) / NARG_PER_BYTE] |= ((id) & 1) ? (nArg) << NARG_BITS :(nArg); \
    }                                                                                      \

    #include "syscall_lookup.h"
#undef SYSCALL_HAND_DEF

SYSCALL_HAND_DEF(__NR_mq_openSysMqOpenmqd_tARG_NUM_4)  
  • g_syscallNArgs为注册函数的参数个数,也会一块记录下来。
  • 四个参数为 SYS_mq_open的四个参数,后续将保存在R0~R3寄存器中

3. syscall

long syscall(long n, ...)
{
	va_list ap;
	syscall_arg_t abcdef;
	va_start(apn);
	a=va_arg(apsyscall_arg_t);
	b=va_arg(apsyscall_arg_t);
	c=va_arg(apsyscall_arg_t);
	d=va_arg(apsyscall_arg_t);
	e=va_arg(apsyscall_arg_t);
	f=va_arg(apsyscall_arg_t);//最多6个参数
	va_end(ap);
	return __syscall_ret(__syscall(nabcdef));
}
//4个参数的系统调用时底层处理
static inline long __syscall4(long n, long along b, long clong 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     SPSP, #(4 * 16)	@先申请16个栈空间用于处理本次软中断
    STMIA   SP, {R0-R12}		@TaskContext.R[GEN_REGS_NUM] STMIA从左到右执行先放R0 .. R12
    MRS     R3SPSR			@读取本模式下的SPSR值
    MOV     R4LR				@保存回跳寄存器LR

    AND     R1R3#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 modewe 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     R0SP											 @获取SP值R0将作为OsArmA32SyscallHandle的参数
    STMFD   SP!, {R3}                                        @ Save the CPSR 入栈保存CPSR值 => TaskContext.regPSR
    ADD     R3SP, #(4 * 17)                                @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置
    STMFD   R3!, {R4}                                        @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PC
    STMFD   R3, {R13R14}^                                  @ Save user mode r13(sp) and r14(lr) 从右向左 保存 => TaskContext.LR和SP
    SUB     SPSP#4										 @ => TaskContext.resved
    PUSH_FPU_REGS R1	@保存中断模式(用户模式模式)											
	@保存任务上下文(TaskContext) 结束
    MOV     FP#0                                           @ Init frame pointer
    CPSIE   I	@开中断表明在系统调用期间可响应中断
    BLX     OsArmA32SyscallHandle	/*交给C语言处理系统调用,参数为R0,指向TaskContext的开始位置*/
    CPSID   I	@执行后续指令前必须先关中断
	@恢复任务上下文(TaskContext) 开始
    POP_FPU_REGS R1											 @弹出FP值给R1
    ADD     SPSP#4										 @ 定位到保存旧SPSR值的位置
    LDMFD   SP!, {R3}                                        @ Fetch the return SPSR 弹出旧SPSR值
    MSR     SPSR_cxsfR3                                    @ Set the return mode SPSR 恢复该模式下的SPSR值

    @ we are leaving to user modewe 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, {R13R14}^                                  @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器
    ADD     SPSP, #(2 * 4)								 @定位到保存旧PC值的位置
    LDMFD   SP!, {PC}^                                       @ Return to user 切回用户模式运行
	@恢复任务上下文(TaskContext) 结束

OsKernelSVCHandler:@主要目的是保存ExcContext中除(R0~R12)的其他寄存器
    ADD     R0SP, #(4 * 16)	@跳转到保存PCLRSP的位置此时R0位置刚好是SP的位置
    MOV     R5R0				@由R5记录SP位置因为R0要暂时充当SP寄存器来使用
    STMFD   R0!, {R4}                                        @ Store PC => ExcContext.PC
    STMFD   R0!, {R4}										 @ 相当于保存了=>  ExcContext.LR
    STMFD   R0!, {R5}										 @ 相当于保存了=>  ExcContext.SP

    STMFD   SP!, {R3}                                        @ Push task`s CPSR (i.e. exception SPSR). =>ExcContext.regPSR
    SUB     SPSP, #(4 * 2)                                 @ user sp and lr => =>ExcContext.USPULR

    MOV     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    OsArmA32SyscallHandle
SYSCALL是产生系统调用时触发的信号,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"cmdnArgs);
        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. SysMqOpen

mqd_t SysMqOpen(const char *mqNameint openFlagmode_t modestruct mq_attr *attr)
{
    mqd_t ret;
    int retValue;
    char kMqName[PATH_MAX + 1] = { 0 };

    retValue = LOS_StrncpyFromUser(kMqNamemqNamePATH_MAX);
    if (retValue < 0) {
        return retValue;
    }
    ret = mq_open(kMqNameopenFlagmodeattr);//一个消息队列可以有多个进程向它读写消息
    if (ret == -1) {
        return (mqd_t)-get_errno();
    }
    return ret;
}

解读

  • 此处的mq_open和main函数的mq_open其实是两个函数体实现。一个是给应用层的调用,一个是内核层使用,只是名字一样而已。
  • SysMqOpen是返回到 OsArmA32SyscallHandle regs[REG_R0] = ret;
  • OsArmA32SyscallHandle再返回到 _osExceptSwiHdl
  • _osExceptSwiHdl后面的代码是用于恢复用户模式现场和SPSRPC 等寄存器。

以上为鸿蒙系统调用的整个过程。
关于寄存器(R0~R15)在每种模式下的使用方式,后续将由其他篇详细说明,敬请关注。

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数 调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析 任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈 内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存 文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝 芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理 编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断
编译运行 调测工具
编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO 模块监控 日志跟踪 系统安全 测试用例

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

期间不断得到小伙伴的支持,有学生,有职场新人,也有老江湖,在此一并感谢,大家的支持是前进的动力。尤其每次收到学生的赞助很感慨,后生可敬。 >> 查看捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。:)

Clone this wiki locally