本帖最后由 l_xy 于 2020-10-18 17:29 编辑
写在前面我也第一时间从码云下载了鸿蒙系统的源代码,并进行了编译和分析。当晚回看了HDC上的关于鸿蒙OS 2.0的主题演讲,个人最为好奇的是——这次开源的liteos-a内核。因为它支持了带MMU(内存管理单元)的 ARM Cortex-A设备;我们知道,在带有MMU的处理器上,可以实现虚拟内存,进而实现进程之间的隔离、内核态和用户态的隔离等等这些功能。
系统调用简介引用一张官方文档中的图片,看看liteos-a内核在整个系统中的位置。 这次开源的鸿蒙系统中同时包含了两个内核,分别是liteos-a和liteos-m,其中的liteos-m和以前开源的LiteOS相当,而liteos-a是面向应用处理器的操作系统内核,提供了更为丰富的内核功能。此前已经开源的LiteOS,只是一个实时操作系统(RTOS),它主要面向的是内存和闪存配置都比较低的微控制器。 我们先来简单回顾一下操作系统课程的一个知识点——系统调用,以及为什么会有系统调用?它的作用是什么?如果你对于这两个问题以及了然于心,可以直接跳过本段,看后面的源码分析部分。 在微控制器这样的系统资源较少的硬件系统(比如 STM32、MSP430、AVR、8051)上,通常直接裸跑程序(也就是不使用任何操作系统),或者使用像FreeRTOS、Zephyr这一类的实时操作系统(RTOS)。这些实时操作系统中,应用程序和内核程序直接运行在同一个物理内存空间(因为这些设备一般没有MMU)上。而RTOS只提供了线程(或者叫任务),线程间同步、互斥等基础设施;应用程序可以直接调用内核函数(用户程序和内核程序只是逻辑上的划分,本质上并没有太大不同);一旦有一个线程发生异常,整个系统就会重启。 而在ARM Cortex-A、x86、x86-64这样的系统资源丰富的硬件系统上,SoC或CPU芯片内部一般集成了MMU,而且CPU有特权级别状态(状态寄存器的某些位)。基于特权级别状态,可以实现部分硬件相关的操作只能在内核态进行,例如访问外设等,用户态应用程序不能访问硬件设备。在这样的系统上,系统调用是用户态应用程序调用内核功能的请求入口。通俗的说,系统调用就是在有内核态和用户态隔离的操作系统上,用户态进程访问内核态资源的一种方式。
从Hello World开始接下来,我们一起从鸿蒙系统源码分析它在liteos-a内核上是如何实现系统调用的。鸿蒙OS使用了musl libc,应用程序和系统服务都通过musl libc封装的系统调用API接口访问内核相关功能。 下面,我们就从经典的helloworld分析整个系统调用的流程。鸿蒙系统目前官方支持了三个芯片平台,分别是Hi3516DV300(双核ARM Cortex A-7 @ 900M Hz),Hi3518EV300(单核ARM Cortex A-7 @ 900MHz 内置64MB DDR2内存)和Hi3861V100(单核RISC-V @160M Hz 内置 SRAM 和 Flash)。其中Hi3516和Hi3518是带有Cortex A7内核的芯片,鸿蒙系统在这两个平台使用的内核自然是liteos-a。根据官方指导文档,我们知道这两个平台的第一个应用程序示例都是helloworld,源码路径为:applica tions/sample/camera/app/src/helloworld.c,除去头部注释,代码内容为:
- #include
- #include "los_sample.h"
-
- int main(int argc, char **argv)
- {
- printf("n************************************************n");
- printf("nttHello OHOS!n");
- printf("n************************************************nn");
-
- LOS_Sample(g_num);
-
- return 0;
- }
复制代码
musl libc的printf函数实现分析文件路径:third_party/musl/src/stdio/printf.c:
- int printf(const char *restrict fmt, ...)
- {
- int ret;
- va_list ap;
- va_start(ap, fmt);
- ret = vfprintf(stdout, fmt, ap);
- va_end(ap);
- return ret;
- }
复制代码
我们看到了,这里使用标准库的stdout作为第一个参数调用了vfprintf,我们继续向下分析third_party/musl/src/stdio/vfprintf.c文件:
- int vfprintf(FILE *restrict f, const char *restrict fmt, va_list ap)
- {
- // 删减若干和参数 f 无关的代码行
- FLOCK(f);
- olderr = f->flags & F_ERR;
- if (f->mode < 1) f->flags &= ~F_ERR;
- if (!f->buf_size) {
- saved_buf = f->buf;
- f->buf = internal_buf;
- f->buf_size = sizeof internal_buf;
- f->wpos = f->wbase = f->wend = 0;
- }
- if (!f->wend && __towrite(f)) ret = -1;
- else ret = printf_core(f, fmt, &ap2, nl_arg, nl_type);
- if (saved_buf) {
- f->write(f, 0, 0);
- if (!f->wpos) ret = -1;
- f->buf = saved_buf;
- f->buf_size = 0;
- f->wpos = f->wbase = f->wend = 0;
- }
- if (f->flags & F_ERR) ret = -1;
- f->flags |= olderr;
- FUNLOCK(f);
- va_end(ap2);
- return ret;
- }
复制代码
这里,我们继续关注三处带有参数f的调用:__towrite(f),printf_core(f, fmt, &ap2, nl_arg, nl_type),f->write(f, 0, 0); 其中,__towrite的实现位于third_party/musl/src/stdio/__towrite.c(可见和系统调用无关):
- int __towrite(FILE *f)
- {
- f->mode |= f->mode-1;
- if (f->flags & F_NOWR) {
- f->flags |= F_ERR;
- return EOF;
- }
- /* Clear read buffer (easier than summoning nasal demons) */
- f->rpos = f->rend = 0;
-
- /* Activate write through the buffer. */
- f->wpos = f->wbase = f->buf;
- f->wend = f->buf + f->buf_size;
-
- return 0;
- }
复制代码
从内容上看,__towrite函数的作用是更新文件结构FILE的wpos、wbase、wend成员,以指向待写入实际文件的内存缓冲区域,同时将rpos、rend值为零。 printf_core的实现也位于src/stdio/vfprintf.c文件:
- static int printf_core(FILE *f, const char *fmt, va_list *ap, union arg *nl_arg, int *nl_type)
- {
- // 删除了变量定义部分
- for (;;) {
- /* This error is only specified for snprintf, but since it's
- * unspecified for other forms, do the same. Stop immediately
- * on overflow; otherwise %n could produce wrong results. */
- if (l > INT_MAX - cnt) goto overflow;
-
- /* Update output count, end loop when fmt is exhausted */
- cnt += l;
- if (!*s) break;
-
- /* Handle literal text and %% format specifiers */
- for (a=s; *s && *s!='%'; s++);
- for (z=s; s[0]=='%' && s[1]=='%'; z++, s+=2);
- if (z-a > INT_MAX-cnt) goto overflow;
- l = z-a;
- if (f) out(f, a, l);
- if (l) continue;
-
- if (isdigit(s[1]) && s[2]=='
- [align=left]从注释和代码结构可以看出,这个函数实现了格式化字符串展开的主要流程,这里又调用了out和pad两个函数,从命名猜测应该分别是向内存缓冲区写入内容和填充内容的函数,它们的实现也位于vfprintf.c中:[/align][code]static void out(FILE *f, const char *s, size_t l)
- {
- if (!(f->flags & F_ERR)) __fwritex((void *)s, l, f);
- }
-
- static void pad(FILE *f, char c, int w, int l, int fl)
- {
- char pad[256];
- if (fl & (LEFT_ADJ | ZERO_PAD) || l >= w) return;
- l = w - l;
- memset(pad, c, l>sizeof pad ? sizeof pad : l);
- for (; l >= sizeof pad; l -= sizeof pad)
- out(f, pad, sizeof pad);
- out(f, pad, l);
- }
复制代码
它们又调用了__fwritex,它的实现位于third_party/musl/src/stdio/fwrite.c:
- size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f)
- {
- size_t i=0;
-
- if (!f->wend && __towrite(f)) return 0;
-
- if (l > f->wend - f->wpos) return f->write(f, s, l);
-
- if (f->lbf >= 0) {
- /* Match /^(.*n|)/ */
- for (i=l; i && s[i-1] != 'n'; i--);
- if (i) {
- size_t n = f->write(f, s, i);
- if (n < i) return n;
- s += i;
- l -= i;
- }
- }
-
- memcpy(f->wpos, s, l);
- f->wpos += l;
- return l+i;
- }
复制代码
这里又出现了vfprintf中出现的f->write(f, s, i),下面我们就分析这个函数实际底是什么? 我们先找到它的定义prebuilts/lite/sysroot/usr/include/arm-liteos/bits/alltypes.h:
- #if defined(__NEED_FILE) && !defined(__DEFINED_FILE)
- typedef struct _IO_FILE FILE;
- #define __DEFINED_FILE
- #endif
复制代码
以及third_party/musl/src/internal/stdio_impl.h:
- struct _IO_FILE {
- unsigned flags;
- unsigned char *rpos, *rend;
- int (*close)(FILE *);
- unsigned char *wend, *wpos;
- unsigned char *mustbezero_1;
- unsigned char *wbase;
- size_t (*read)(FILE *, unsigned char *, size_t);
- size_t (*write)(FILE *, const unsigned char *, size_t); // <--关注它
- off_t (*seek)(FILE *, off_t, int);
- unsigned char *buf;
- size_t buf_size;
- FILE *prev, *next;
- int fd;
- int pipe_pid;
- long lockcount;
- int mode;
- volatile int lock;
- int lbf;
- void *cookie;
- off_t off;
- char *getln_buf;
- void *mustbezero_2;
- unsigned char *shend;
- off_t shlim, shcnt;
- FILE *prev_locked, *next_locked;
- struct __locale_struct *locale;
- };
复制代码
我们再继续寻找stdout的各个成员值是什么? 可以找到third_party/musl/src/stdio/stdout.c文件中的:
- static unsigned char buf[BUFSIZ+UNGET];
- hidden FILE __stdout_FILE = {
- .buf = buf+UNGET,
- .buf_size = sizeof buf-UNGET,
- .fd = 1, // fd 为 1 和多数UNIX系统一样
- .flags = F_PERM | F_NORD,
- .lbf = 'n',
- .write = __stdout_write, // <-- write 成员在这里
- .seek = __stdio_seek,
- .close = __stdio_close,
- .lock = -1,
- };
复制代码
FILE *const stdout = &__stdout_FILE; // <-- stdout 在这里 third_party/musl/src/stdio/__stdout_write.c文件中:
- size_t __stdout_write(FILE *f, const unsigned char *buf, size_t len)
- {
- struct winsize wsz;
- f->write = __stdio_write;
- if (!(f->flags & F_SVB) && __syscall(SYS_ioctl, f->fd, TIOCGWINSZ, &wsz))
- f->lbf = -1;
- return __stdio_write(f, buf, len);
- }
复制代码
这段代码里调用了SYS_ioctl系统调用,但主体流程是下方的函数__stdio_write,它的实现在third_party/musl/src/stdio/__stdio_write.c文件中:
- size_t __stdio_write(FILE *f, const unsigned char *buf, size_t len)
- {
- struct iovec iovs[2] = {
- { .iov_base = f->wbase, .iov_len = f->wpos-f->wbase },
- { .iov_base = (void *)buf, .iov_len = len }
- };
- struct iovec *iov = iovs;
- size_t rem = iov[0].iov_len + iov[1].iov_len;
- int iovcnt = 2;
- ssize_t cnt;
- for (;;) {
- cnt = syscall(SYS_writev, f->fd, iov, iovcnt); // <-- 看这里!
- if (cnt == rem) {
- f->wend = f->buf + f->buf_size;
- f->wpos = f->wbase = f->buf;
- return len;
- }
- if (cnt < 0) {
- f->wpos = f->wbase = f->wend = 0;
- f->flags |= F_ERR;
- return iovcnt == 2 ? 0 : len-iov[0].iov_len;
- }
- rem -= cnt;
- if (cnt > iov[0].iov_len) {
- cnt -= iov[0].iov_len;
- iov++; iovcnt--;
- }
- iov[0].iov_base = (char *)iov[0].iov_base + cnt;
- iov[0].iov_len -= cnt;
- }
- }
复制代码
至此,我们看到了printf函数最终调用到了两个系统调用SYS_ioctl和SYS_write。 musl libc的syscall函数实现分析在上一节中,我们看到printf最终调用到了两个长得像系统调用的函数syscall和__syscall。 系统调用宏syscall的实现在musl代码仓(third_party/musl)下搜索:
$ find . -name '*.h' | xargs grep --color -n 'ssyscall('
./kernel/include/unistd.h:198:long syscall(long, ...);
./src/internal/syscall.h:44:#define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))
./include/unistd.h:199:long syscall(long, ...);
可以找到third_party/musl/src/internal/syscall.h:
- #define __syscall(...) __SYSCALL_DISP(__syscall,__VA_ARGS__)
- #define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))
复制代码
这里可以看到它们两者都是宏,而syscall调用了__syscall,而__syscall又调用了__SYSCALL_DISP,它的实现也在同一个文件中:
- #define __SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n
- #define __SYSCALL_NARGS(...) __SYSCALL_NARGS_X(__VA_ARGS__,7,6,5,4,3,2,1,0,)
- #define __SYSCALL_CONCAT_X(a,b) a##b
- #define __SYSCALL_CONCAT(a,b) __SYSCALL_CONCAT_X(a,b)
- #define __SYSCALL_DISP(b,...) __SYSCALL_CONCAT(b,__SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)
复制代码
我们以__stdio_write中调用syscall处进行分析,即尝试展开syscall(SYS_writev, f->fd, iov, iovcnt);
- syscall(SYS_writev, f->fd, iov, iovcnt);
- => __syscall_ret(__syscall(SYS_writev, f->fd, iov, iovcnt)) // 展开syscall
- => __syscall_ret(__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)); // 展开__syscall
复制代码
先忽略最外层的 __syscall_ret,展开__SYSCALL_DISP部分:
- __SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)
- => __SYSCALL_CONCAT(__syscall, __SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt))(SYS_writev, f->fd, iov, iovcnt) // 展开 __SYSCALL_DISP
复制代码
忽略外层的__SYSCALL_CONCAT,展开__SYSCALL_NARGS_X部分:
- __SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt)
- => __SYSCALL_NARGS_X(SYS_writev, f->fd, iov, iovcnt,7,6,5,4,3,2,1,0,) // 展开 __SYSCALL_NARGS
- => 3 // 展开 __SYSCALL_NARGS_X
- // SYS_writev, f->fd, iov, iovcnt 和宏参数 a,b,c,d 对应
- // 7,6,5,4 和宏参数 e,f,g,h 对应
- // 3 和宏参数 n 对应
- // 宏表达式的值为 n 也就是 3,
复制代码
回到__SYSCALL_CONCAT 展开流程,
- __SYSCALL_CONCAT(__syscall, __SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt))
- => __SYSCALL_CONCAT(__syscall, 3)
- => __SYSCALL_CONCAT_X(__syscall, 3)
- => __syscall3
复制代码
再回到__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)展开流程,结果应该是:
- __SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)
- => __syscall3(SYS_writev, f->fd, iov, iovcnt)
复制代码
系统调用函数__syscall3的实现这些__syscall[1-7]的系统调用包装宏定义如下:
- #ifndef __scc
- #define __scc(X) ((long) (X)) // 转为long类型
- typedef long syscall_arg_t;
- #endif
-
- #define __syscall1(n,a) __syscall1(n,__scc(a))
- #define __syscall2(n,a,b) __syscall2(n,__scc(a),__scc(b))
- #define __syscall3(n,a,b,c) __syscall3(n,__scc(a),__scc(b),__scc(c)) // <- 看这里
- #define __syscall4(n,a,b,c,d) __syscall4(n,__scc(a),__scc(b),__scc(c),__scc(d))
- #define __syscall5(n,a,b,c,d,e) __syscall5(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e))
- #define __syscall6(n,a,b,c,d,e,f) __syscall6(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f))
- #define __syscall7(n,a,b,c,d,e,f,g) __syscall7(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f),__scc(g))
复制代码
继续搜索发现有多出匹配,我们关注arch/arm目录下的文件,因为ARM Cortext A7是Armv7-A指令集的32位CPU(如果是Armv8-A指令集的64位CPU则对应arch/aarch64下的文件):
- static inline long __syscall3(long n, long a, long b, long c)
- {
- register long r7 __ASM____R7__ = n;
- register long r0 __asm__("r0") = a;
- register long r1 __asm__("r1") = b;
- register long r2 __asm__("r2") = c;
- __asm_syscall(R7_OPERAND, "0"(r0), "r"(r1), "r"(r2));
- }
复制代码
这段代码中还有三个宏,__ASM____R7__、__asm_syscall和R7_OPERAND:
- #ifdef __thumb__
-
- #define __ASM____R7__
- #define __asm_syscall(...) do {
- __asm__ __volatile__ ( "mov %1,r7 ; mov r7,%2 ; svc 0 ; mov r7,%1"
- : "=r"(r0), "=&r"((int){0}) : __VA_ARGS__ : "memory");
- return r0;
- } while (0)
-
- #else // __thumb__
-
- #define __ASM____R7__ __asm__("r7")
- #define __asm_syscall(...) do {
- __asm__ __volatile__ ( "svc 0"
- : "=r"(r0) : __VA_ARGS__ : "memory");
- return r0;
- } while (0)
- #endif // __thumb__
-
- #ifdef __thumb2__
- #define R7_OPERAND "rI"(r7)
- #else
- #define R7_OPERAND "r"(r7)
- #endif
复制代码
它们有两个实现版,分别对应于编译器THUMB选项的开启和关闭。这两种选项条件下的代码流程基本一致,以下仅以未开启THUMB选项为例进行分析。这两个宏展开后的__syscall3函数内容为:
- static inline long __syscall3(long n, long a, long b, long c)
- {
- register long r7 __asm__("r7") = n; // 系统调用号
- register long r0 __asm__("r0") = a; // 参数0
- register long r1 __asm__("r1") = b; // 参数1
- register long r2 __asm__("r2") = c; // 参数2
- do {
- __asm__ __volatile__ ( "svc 0"
- : "=r"(r0) : "r"(r7), "0"(r0), "r"(r1), "r"(r2) : "memory");
- return r0;
- } while (0);
- }
复制代码
这里最后的一个内嵌汇编比较复杂,它符合如下格式(具体细节可以查阅gcc内嵌汇编文档的扩展汇编说明):
- asm asm-qualifiers ( AssemblerTemplate
- : OutputOperands
- [ : InputOperands
- [ : Clobbers ] ])
复制代码
汇编模板为:"svc 0", 输出参数部分为:"=r"(r0),输出寄存器为r0 输入参数部分为:"r"(r7), "0"(r0), "r"(r1), "r"(r2),输入寄存器为r7,r0,r1,r2,("0"的含义是,这个输入寄存器必须和输出寄存器第0个位置一样) Clobber部分为:"memory" 这里我们只需要记住:系统调用号存放在r7寄存器,参数存放在r0,r1,r2,返回值最终会存放在r0中; SVC指令,ARM Cortex A7手册 的解释为: The SVC instruction causes a Supervisor Call exception. This provides a mechanism for unprivileged software to make a call to the operating system, or other system component that is accessible only at PL1.
翻译过来就是说 SVC指令会触发一个“特权调用”异常。这为非特权软件调用操作系统或其他只能在PL1级别访问的系统组件提供了一种机制。
详细的指令说明在 到这里,我们分析了鸿蒙系统上应用程序如何进入内核态,主要分析的是musl libc的实现。
liteos-a内核的系统调用实现分析既然SVC能够触发一个异常,那么我们就要看看liteos-a内核是如何处理这个异常的。
ARM Cortex A7中断向量表在ARM架构参考手册中,可以找到中断向量表的说明: 可以看到SVC中断向量的便宜地址是0x08,我们可以在kernel/liteos_a/arch/arm/arm/src/startup目录的reset_vector_mp.S文件和reset_vector_up.S文件中找到相关汇编代码:
- __exception_handlers:
- /*
- *Assumption: ROM code has these vectors at the hardware reset address.
- *A simple jump removes any address-space dependencies [i.e. safer]
- */
- b reset_vector
- b _osExceptUndefInstrHdl
- b _osExceptSwiHdl
- b _osExceptPrefetchAbortHdl
- b _osExceptDataAbortHdl
- b _osExceptAddrAbortHdl
- b OsIrqHandler
- b _osExceptFiqHdl
- PS: kernel/liteos_a/arch/arm/arm/src/startup目录有两个文件reset_vector_mp.S文件和reset_vector_up.S文件分别对应多核和单核编译选项:
- ifeq ($(LOSCFG_KERNEL_SMP), y)
- LOCAL_SRCS += src/startup/reset_vector_mp.S
- else
- LOCAL_SRCS += src/startup/reset_vector_up.S
- endif
复制代码
SVC中断处理函数上面的汇编代码中可以看到,_osExceptSwiHdl函数就是SVC异常处理函数,具体实现在kernel/liteos_a/arch/arm/arm/src/los_hw_exc.S文件中:
- @ Description: Software interrupt exception handler
- _osExceptSwiHdl:
- SUB SP, SP, #(4 * 16) @ 栈增长
- STMIA SP, {R0-R12} @ 保存R0-R12寄存器到栈上
- MRS R3, SPSR @ 移动SPSR寄存器的值到R3
- MOV R4, LR
-
- AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode
- CMP R1, #CPSR_USER_MODE @ User mode
- BNE OsKernelSVCHandler @ Branch if not user mode
-
- @ 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
- STMFD SP!, {R3} @ Save the CPSR
- ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage
- STMFD R3!, {R4} @ Save the CPSR and r15(pc)
- STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr)
- SUB SP, SP, #4
- PUSH_FPU_REGS R1
-
- MOV FP, #0 @ Init frame pointer
- CPSIE I @ Interrupt Enable
- BLX OsArmA32SyscallHandle
- CPSID I @ Interrupt Disable
-
- POP_FPU_REGS R1
- ADD SP, SP,#4
- LDMFD SP!, {R3} @ Fetch the return SPSR
- MSR SPSR_cxsf, R3 @ Set the return mode 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}
- LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14
- ADD SP, SP, #(2 * 4)
- LDMFD SP!, {PC}^ @ Return to user
复制代码
这段代码的注释较为清楚,可以看到,内核模式会继续调用OsKernelSVCHandler,用户模式会继续调用OsArmA32SyscallHandle函数;
OsArmA32SyscallHandle函数我们这里分析的流程是从用户模式进入的,所以调用的是OsArmA32SyscallHandle,它的实现位于kernel/liteos_a/syscall/los_syscall.c文件:
- /* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */
- LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs)
- {
- UINT32 ret;
- UINT8 nArgs;
- UINTPTR handle;
- UINT32 cmd = regs[REG_R7];
-
- if (cmd >= SYS_CALL_NUM) {
- PRINT_ERR("Syscall ID: error %d !!!n", cmd);
- return regs;
- }
-
- if (cmd == __NR_sigreturn) {
- OsRestorSignalContext(regs);
- return regs;
- }
-
- handle = g_syscallHandle[cmd]; // 得到实际系统调用处理函数
- 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)) {
- PRINT_ERR("Unsupport syscall ID: %d nArgs: %dn", cmd, nArgs);
- regs[REG_R0] = -ENOSYS;
- return regs;
- }
-
- switch (nArgs) { // 以下各个case是实际函数调用
- case ARG_NUM_0:
- case ARG_NUM_1:
- ret = (*(SyscallFun1)handle)(regs[REG_R0]);
- break;
- case ARG_NUM_2:
- case ARG_NUM_3:
- ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);
- 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:
- 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;
- }
复制代码
这个函数中用到了个全局数组g_syscallHandle和g_syscallNArgs,它们的定义以及初始化函数也在同一个文件中:
- static UINTPTR g_syscallHandle[SYS_CALL_NUM] = {0};
- static UINT8 g_syscallNArgs[(SYS_CALL_NUM + 1) / NARG_PER_BYTE] = {0};
-
- void SyscallHandleInit(void)
- {
- #define SYSCALL_HAND_DEF(id, fun, rType, nArg)
- 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宏的对齐格式我做了一点调整。 从g_syscallNArgs成员赋值以及定义的地方,能看出它的每个UINT8成员被用来存放两个系统调用的参数个数,从而实现更少的内存占用; syscall_lookup.h文件和los_syscall.c位于同一目录,它记录了系统调用函数对照表,我们仅节取一部分:
- SYSCALL_HAND_DEF(__NR_read, SysRead, ssize_t, ARG_NUM_3)
- SYSCALL_HAND_DEF(__NR_write, SysWrite, ssize_t, ARG_NUM_3) // <-- 我们要跟踪的 write 在这里
- SYSCALL_HAND_DEF(__NR_open, SysOpen, int, ARG_NUM_7)
- SYSCALL_HAND_DEF(__NR_close, SysClose, int, ARG_NUM_1)
- SYSCALL_HAND_DEF(__NR_creat, SysCreat, int, ARG_NUM_2)
- SYSCALL_HAND_DEF(__NR_unlink, SysUnlink, int, ARG_NUM_1)
-
- #ifdef LOSCFG_KERNEL_DYNLOAD
- SYSCALL_HAND_DEF(__NR_execve, SysExecve, int, ARG_NUM_3)
- #endif
复制代码
看到这里,write系统调用的内核函数终于找到了——SysWrite。 到此,我们已经知道了liteos-a的系统调用机制是如何实现的。
liteos-a内核SysWrite的实现SysWrite函数的实现位于kernel/liteos_a/syscall/fs_syscall.c文件:
- ssize_t SysWrite(int fd, const void *buf, size_t nbytes)
- {
- int ret;
-
- if (nbytes == 0) {
- return 0;
- }
-
- if (!LOS_IsUserAddressRange((vaddr_t)(UINTPTR)buf, nbytes)) {
- return -EFAULT;
- }
-
- /* Process fd convert to system global fd */
- fd = GetAssociatedSystemFd(fd);
-
- ret = write(fd, buf, nbytes); // <-- ??似曾相识??
- if (ret < 0) {
- return -get_errno();
- }
- return ret;
- }
复制代码
它又调用了write?但是这一次是内核空间的write,不再是 musl libc,经过一番搜索,我们可以找到另一个文件third_party/NuttX/fs/vfs/fs_write.c中的write:
- ssize_t write(int fd, FAR const void *buf, size_t nbytes) {
- #if CONFIG_NFILE_DESCRIPTORS > 0
- FAR struct file *filep;
- if ((unsigned int)fd >= CONFIG_NFILE_DESCRIPTORS)
- #endif
- { /* Write to a socket descriptor is equivalent to send with flags == 0 */
- #if defined(LOSCFG_NET_LWIP_SACK)
- FAR const void *bufbak = buf;
- ssize_t ret;
- if (LOS_IsUserAddress((VADDR_T)(uintptr_t)buf)) {
- if (buf != NULL && nbytes > 0) {
- buf = malloc(nbytes);
- if (buf == NULL) { /* 省略 错误处理 代码 */ }
- if (LOS_ArchCopyFromUser((void*)buf, bufbak, nbytes) != 0) {/* 省略 */}
- }
- }
- ret = send(fd, buf, nbytes, 0); // 这个分支是处理socket fd的
- if (buf != bufbak) {
- free((void*)buf);
- }
- return ret;
- #else
- set_errno(EBADF);
- return VFS_ERROR;
- #endif
- }
-
- #if CONFIG_NFILE_DESCRIPTORS > 0
- /* The descriptor is in the right range to be a file descriptor... write
- * to the file.
- */
- if (fd <= STDERR_FILENO && fd >= STDIN_FILENO) { /* fd : [0,2] */
- fd = ConsoleUpdateFd();
- if (fd < 0) {
- set_errno(EBADF);
- return VFS_ERROR;
- }
- }
-
- int ret = fs_getfilep(fd, &filep);
- if (ret < 0) {
- /* The errno value has already been set */
- return VFS_ERROR;
- }
-
- if (filep->f_oflags & O_DIRECTORY) {
- set_errno(EBADF);
- return VFS_ERROR;
- }
-
- if (filep->f_oflags & O_APPEND) {
- if (file_seek64(filep, 0, SEEK_END) == -1) {
- return VFS_ERROR;
- }
- }
-
- /* Perform the write operation using the file descriptor as an index */
- return file_write(filep, buf, nbytes);
- #endif
- }
复制代码
找到这段代码,我们知道了: liteos-a的vfs是在NuttX基础上实现的,NuttX是一个开源RTOS项目; liteos-a的TCP/IP协议栈是基于lwip的,lwip也是一个开源项目; 这段代码中的write分为两个分支,socket fd调用lwip的send,另一个分支调用file_write;
至于,file_write如何调用到存储设备驱动程序,则是更底层的实现了,本文不在继续分析。
补充说明本文内容均是基于鸿蒙系统开源项目OpenHarmony源码静态分析所整理,没有进行实际的运行环境调试,实际执行过程可能有所差异,希望发现错误的读者及时指正。文中所有路径均为整个openharmony源码树上的相对路径(而非liteos源码相对路径)。
参考链接
) {
l10n=1;
argpos = s[1]-'0';
s+=3;
} else {
argpos = -1;
s++;
}
/* Read modifier flags */
for (fl=0; (unsigned)*s-' '<32 && (FLAGMASK&(1U<<*s-' ')); s++)
fl |= 1U<<*s-' ';
/* Read field width */
if (*s=='*') {
if (isdigit(s[1]) && s[2]=='
从注释和代码结构可以看出,这个函数实现了格式化字符串展开的主要流程,这里又调用了out和pad两个函数,从命名猜测应该分别是向内存缓冲区写入内容和填充内容的函数,它们的实现也位于vfprintf.c中:
- static void out(FILE *f, const char *s, size_t l)
- {
- if (!(f->flags & F_ERR)) __fwritex((void *)s, l, f);
- }
-
- static void pad(FILE *f, char c, int w, int l, int fl)
- {
- char pad[256];
- if (fl & (LEFT_ADJ | ZERO_PAD) || l >= w) return;
- l = w - l;
- memset(pad, c, l>sizeof pad ? sizeof pad : l);
- for (; l >= sizeof pad; l -= sizeof pad)
- out(f, pad, sizeof pad);
- out(f, pad, l);
- }
复制代码
它们又调用了__fwritex,它的实现位于third_party/musl/src/stdio/fwrite.c:
- size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f)
- {
- size_t i=0;
-
- if (!f->wend && __towrite(f)) return 0;
-
- if (l > f->wend - f->wpos) return f->write(f, s, l);
-
- if (f->lbf >= 0) {
- /* Match /^(.*n|)/ */
- for (i=l; i && s[i-1] != 'n'; i--);
- if (i) {
- size_t n = f->write(f, s, i);
- if (n < i) return n;
- s += i;
- l -= i;
- }
- }
-
- memcpy(f->wpos, s, l);
- f->wpos += l;
- return l+i;
- }
复制代码
这里又出现了vfprintf中出现的f->write(f, s, i),下面我们就分析这个函数实际底是什么? 我们先找到它的定义prebuilts/lite/sysroot/usr/include/arm-liteos/bits/alltypes.h:
- #if defined(__NEED_FILE) && !defined(__DEFINED_FILE)
- typedef struct _IO_FILE FILE;
- #define __DEFINED_FILE
- #endif
复制代码
以及third_party/musl/src/internal/stdio_impl.h:
- struct _IO_FILE {
- unsigned flags;
- unsigned char *rpos, *rend;
- int (*close)(FILE *);
- unsigned char *wend, *wpos;
- unsigned char *mustbezero_1;
- unsigned char *wbase;
- size_t (*read)(FILE *, unsigned char *, size_t);
- size_t (*write)(FILE *, const unsigned char *, size_t); // <--关注它
- off_t (*seek)(FILE *, off_t, int);
- unsigned char *buf;
- size_t buf_size;
- FILE *prev, *next;
- int fd;
- int pipe_pid;
- long lockcount;
- int mode;
- volatile int lock;
- int lbf;
- void *cookie;
- off_t off;
- char *getln_buf;
- void *mustbezero_2;
- unsigned char *shend;
- off_t shlim, shcnt;
- FILE *prev_locked, *next_locked;
- struct __locale_struct *locale;
- };
复制代码
我们再继续寻找stdout的各个成员值是什么? 可以找到third_party/musl/src/stdio/stdout.c文件中的:
- static unsigned char buf[BUFSIZ+UNGET];
- hidden FILE __stdout_FILE = {
- .buf = buf+UNGET,
- .buf_size = sizeof buf-UNGET,
- .fd = 1, // fd 为 1 和多数UNIX系统一样
- .flags = F_PERM | F_NORD,
- .lbf = 'n',
- .write = __stdout_write, // <-- write 成员在这里
- .seek = __stdio_seek,
- .close = __stdio_close,
- .lock = -1,
- };
复制代码
FILE *const stdout = &__stdout_FILE; // <-- stdout 在这里 third_party/musl/src/stdio/__stdout_write.c文件中:
- size_t __stdout_write(FILE *f, const unsigned char *buf, size_t len)
- {
- struct winsize wsz;
- f->write = __stdio_write;
- if (!(f->flags & F_SVB) && __syscall(SYS_ioctl, f->fd, TIOCGWINSZ, &wsz))
- f->lbf = -1;
- return __stdio_write(f, buf, len);
- }
复制代码
这段代码里调用了SYS_ioctl系统调用,但主体流程是下方的函数__stdio_write,它的实现在third_party/musl/src/stdio/__stdio_write.c文件中:
- size_t __stdio_write(FILE *f, const unsigned char *buf, size_t len)
- {
- struct iovec iovs[2] = {
- { .iov_base = f->wbase, .iov_len = f->wpos-f->wbase },
- { .iov_base = (void *)buf, .iov_len = len }
- };
- struct iovec *iov = iovs;
- size_t rem = iov[0].iov_len + iov[1].iov_len;
- int iovcnt = 2;
- ssize_t cnt;
- for (;;) {
- cnt = syscall(SYS_writev, f->fd, iov, iovcnt); // <-- 看这里!
- if (cnt == rem) {
- f->wend = f->buf + f->buf_size;
- f->wpos = f->wbase = f->buf;
- return len;
- }
- if (cnt < 0) {
- f->wpos = f->wbase = f->wend = 0;
- f->flags |= F_ERR;
- return iovcnt == 2 ? 0 : len-iov[0].iov_len;
- }
- rem -= cnt;
- if (cnt > iov[0].iov_len) {
- cnt -= iov[0].iov_len;
- iov++; iovcnt--;
- }
- iov[0].iov_base = (char *)iov[0].iov_base + cnt;
- iov[0].iov_len -= cnt;
- }
- }
复制代码
至此,我们看到了printf函数最终调用到了两个系统调用SYS_ioctl和SYS_write。 musl libc的syscall函数实现分析在上一节中,我们看到printf最终调用到了两个长得像系统调用的函数syscall和__syscall。 系统调用宏syscall的实现在musl代码仓(third_party/musl)下搜索:
$ find . -name '*.h' | xargs grep --color -n 'ssyscall('
./kernel/include/unistd.h:198:long syscall(long, ...);
./src/internal/syscall.h:44:#define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))
./include/unistd.h:199:long syscall(long, ...);
可以找到third_party/musl/src/internal/syscall.h:
- #define __syscall(...) __SYSCALL_DISP(__syscall,__VA_ARGS__)
- #define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))
复制代码
这里可以看到它们两者都是宏,而syscall调用了__syscall,而__syscall又调用了__SYSCALL_DISP,它的实现也在同一个文件中:
- #define __SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n
- #define __SYSCALL_NARGS(...) __SYSCALL_NARGS_X(__VA_ARGS__,7,6,5,4,3,2,1,0,)
- #define __SYSCALL_CONCAT_X(a,b) a##b
- #define __SYSCALL_CONCAT(a,b) __SYSCALL_CONCAT_X(a,b)
- #define __SYSCALL_DISP(b,...) __SYSCALL_CONCAT(b,__SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)
复制代码
我们以__stdio_write中调用syscall处进行分析,即尝试展开syscall(SYS_writev, f->fd, iov, iovcnt);
- syscall(SYS_writev, f->fd, iov, iovcnt);
- => __syscall_ret(__syscall(SYS_writev, f->fd, iov, iovcnt)) // 展开syscall
- => __syscall_ret(__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)); // 展开__syscall
复制代码
先忽略最外层的 __syscall_ret,展开__SYSCALL_DISP部分:
- __SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)
- => __SYSCALL_CONCAT(__syscall, __SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt))(SYS_writev, f->fd, iov, iovcnt) // 展开 __SYSCALL_DISP
复制代码
忽略外层的__SYSCALL_CONCAT,展开__SYSCALL_NARGS_X部分:
- __SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt)
- => __SYSCALL_NARGS_X(SYS_writev, f->fd, iov, iovcnt,7,6,5,4,3,2,1,0,) // 展开 __SYSCALL_NARGS
- => 3 // 展开 __SYSCALL_NARGS_X
- // SYS_writev, f->fd, iov, iovcnt 和宏参数 a,b,c,d 对应
- // 7,6,5,4 和宏参数 e,f,g,h 对应
- // 3 和宏参数 n 对应
- // 宏表达式的值为 n 也就是 3,
复制代码
回到__SYSCALL_CONCAT 展开流程,
- __SYSCALL_CONCAT(__syscall, __SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt))
- => __SYSCALL_CONCAT(__syscall, 3)
- => __SYSCALL_CONCAT_X(__syscall, 3)
- => __syscall3
复制代码
再回到__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)展开流程,结果应该是:
- __SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)
- => __syscall3(SYS_writev, f->fd, iov, iovcnt)
复制代码
系统调用函数__syscall3的实现这些__syscall[1-7]的系统调用包装宏定义如下:
- #ifndef __scc
- #define __scc(X) ((long) (X)) // 转为long类型
- typedef long syscall_arg_t;
- #endif
-
- #define __syscall1(n,a) __syscall1(n,__scc(a))
- #define __syscall2(n,a,b) __syscall2(n,__scc(a),__scc(b))
- #define __syscall3(n,a,b,c) __syscall3(n,__scc(a),__scc(b),__scc(c)) // <- 看这里
- #define __syscall4(n,a,b,c,d) __syscall4(n,__scc(a),__scc(b),__scc(c),__scc(d))
- #define __syscall5(n,a,b,c,d,e) __syscall5(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e))
- #define __syscall6(n,a,b,c,d,e,f) __syscall6(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f))
- #define __syscall7(n,a,b,c,d,e,f,g) __syscall7(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f),__scc(g))
复制代码
继续搜索发现有多出匹配,我们关注arch/arm目录下的文件,因为ARM Cortext A7是Armv7-A指令集的32位CPU(如果是Armv8-A指令集的64位CPU则对应arch/aarch64下的文件):
- static inline long __syscall3(long n, long a, long b, long c)
- {
- register long r7 __ASM____R7__ = n;
- register long r0 __asm__("r0") = a;
- register long r1 __asm__("r1") = b;
- register long r2 __asm__("r2") = c;
- __asm_syscall(R7_OPERAND, "0"(r0), "r"(r1), "r"(r2));
- }
复制代码
这段代码中还有三个宏,__ASM____R7__、__asm_syscall和R7_OPERAND:
- #ifdef __thumb__
-
- #define __ASM____R7__
- #define __asm_syscall(...) do {
- __asm__ __volatile__ ( "mov %1,r7 ; mov r7,%2 ; svc 0 ; mov r7,%1"
- : "=r"(r0), "=&r"((int){0}) : __VA_ARGS__ : "memory");
- return r0;
- } while (0)
-
- #else // __thumb__
-
- #define __ASM____R7__ __asm__("r7")
- #define __asm_syscall(...) do {
- __asm__ __volatile__ ( "svc 0"
- : "=r"(r0) : __VA_ARGS__ : "memory");
- return r0;
- } while (0)
- #endif // __thumb__
-
- #ifdef __thumb2__
- #define R7_OPERAND "rI"(r7)
- #else
- #define R7_OPERAND "r"(r7)
- #endif
复制代码
它们有两个实现版,分别对应于编译器THUMB选项的开启和关闭。这两种选项条件下的代码流程基本一致,以下仅以未开启THUMB选项为例进行分析。这两个宏展开后的__syscall3函数内容为:
- static inline long __syscall3(long n, long a, long b, long c)
- {
- register long r7 __asm__("r7") = n; // 系统调用号
- register long r0 __asm__("r0") = a; // 参数0
- register long r1 __asm__("r1") = b; // 参数1
- register long r2 __asm__("r2") = c; // 参数2
- do {
- __asm__ __volatile__ ( "svc 0"
- : "=r"(r0) : "r"(r7), "0"(r0), "r"(r1), "r"(r2) : "memory");
- return r0;
- } while (0);
- }
复制代码
这里最后的一个内嵌汇编比较复杂,它符合如下格式(具体细节可以查阅gcc内嵌汇编文档的扩展汇编说明):
- asm asm-qualifiers ( AssemblerTemplate
- : OutputOperands
- [ : InputOperands
- [ : Clobbers ] ])
复制代码
汇编模板为:"svc 0", 输出参数部分为:"=r"(r0),输出寄存器为r0 输入参数部分为:"r"(r7), "0"(r0), "r"(r1), "r"(r2),输入寄存器为r7,r0,r1,r2,("0"的含义是,这个输入寄存器必须和输出寄存器第0个位置一样) Clobber部分为:"memory" 这里我们只需要记住:系统调用号存放在r7寄存器,参数存放在r0,r1,r2,返回值最终会存放在r0中; SVC指令,ARM Cortex A7手册 的解释为: The SVC instruction causes a Supervisor Call exception. This provides a mechanism for unprivileged software to make a call to the operating system, or other system component that is accessible only at PL1.
翻译过来就是说 SVC指令会触发一个“特权调用”异常。这为非特权软件调用操作系统或其他只能在PL1级别访问的系统组件提供了一种机制。
详细的指令说明在 到这里,我们分析了鸿蒙系统上应用程序如何进入内核态,主要分析的是musl libc的实现。
liteos-a内核的系统调用实现分析既然SVC能够触发一个异常,那么我们就要看看liteos-a内核是如何处理这个异常的。
ARM Cortex A7中断向量表在ARM架构参考手册中,可以找到中断向量表的说明: 可以看到SVC中断向量的便宜地址是0x08,我们可以在kernel/liteos_a/arch/arm/arm/src/startup目录的reset_vector_mp.S文件和reset_vector_up.S文件中找到相关汇编代码:
- __exception_handlers:
- /*
- *Assumption: ROM code has these vectors at the hardware reset address.
- *A simple jump removes any address-space dependencies [i.e. safer]
- */
- b reset_vector
- b _osExceptUndefInstrHdl
- b _osExceptSwiHdl
- b _osExceptPrefetchAbortHdl
- b _osExceptDataAbortHdl
- b _osExceptAddrAbortHdl
- b OsIrqHandler
- b _osExceptFiqHdl
- PS: kernel/liteos_a/arch/arm/arm/src/startup目录有两个文件reset_vector_mp.S文件和reset_vector_up.S文件分别对应多核和单核编译选项:
- ifeq ($(LOSCFG_KERNEL_SMP), y)
- LOCAL_SRCS += src/startup/reset_vector_mp.S
- else
- LOCAL_SRCS += src/startup/reset_vector_up.S
- endif
复制代码
SVC中断处理函数上面的汇编代码中可以看到,_osExceptSwiHdl函数就是SVC异常处理函数,具体实现在kernel/liteos_a/arch/arm/arm/src/los_hw_exc.S文件中:
- @ Description: Software interrupt exception handler
- _osExceptSwiHdl:
- SUB SP, SP, #(4 * 16) @ 栈增长
- STMIA SP, {R0-R12} @ 保存R0-R12寄存器到栈上
- MRS R3, SPSR @ 移动SPSR寄存器的值到R3
- MOV R4, LR
-
- AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode
- CMP R1, #CPSR_USER_MODE @ User mode
- BNE OsKernelSVCHandler @ Branch if not user mode
-
- @ 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
- STMFD SP!, {R3} @ Save the CPSR
- ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage
- STMFD R3!, {R4} @ Save the CPSR and r15(pc)
- STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr)
- SUB SP, SP, #4
- PUSH_FPU_REGS R1
-
- MOV FP, #0 @ Init frame pointer
- CPSIE I @ Interrupt Enable
- BLX OsArmA32SyscallHandle
- CPSID I @ Interrupt Disable
-
- POP_FPU_REGS R1
- ADD SP, SP,#4
- LDMFD SP!, {R3} @ Fetch the return SPSR
- MSR SPSR_cxsf, R3 @ Set the return mode 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}
- LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14
- ADD SP, SP, #(2 * 4)
- LDMFD SP!, {PC}^ @ Return to user
复制代码
这段代码的注释较为清楚,可以看到,内核模式会继续调用OsKernelSVCHandler,用户模式会继续调用OsArmA32SyscallHandle函数;
OsArmA32SyscallHandle函数我们这里分析的流程是从用户模式进入的,所以调用的是OsArmA32SyscallHandle,它的实现位于kernel/liteos_a/syscall/los_syscall.c文件:
- /* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */
- LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs)
- {
- UINT32 ret;
- UINT8 nArgs;
- UINTPTR handle;
- UINT32 cmd = regs[REG_R7];
-
- if (cmd >= SYS_CALL_NUM) {
- PRINT_ERR("Syscall ID: error %d !!!n", cmd);
- return regs;
- }
-
- if (cmd == __NR_sigreturn) {
- OsRestorSignalContext(regs);
- return regs;
- }
-
- handle = g_syscallHandle[cmd]; // 得到实际系统调用处理函数
- 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)) {
- PRINT_ERR("Unsupport syscall ID: %d nArgs: %dn", cmd, nArgs);
- regs[REG_R0] = -ENOSYS;
- return regs;
- }
-
- switch (nArgs) { // 以下各个case是实际函数调用
- case ARG_NUM_0:
- case ARG_NUM_1:
- ret = (*(SyscallFun1)handle)(regs[REG_R0]);
- break;
- case ARG_NUM_2:
- case ARG_NUM_3:
- ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);
- 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:
- 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;
- }
复制代码
这个函数中用到了个全局数组g_syscallHandle和g_syscallNArgs,它们的定义以及初始化函数也在同一个文件中:
- static UINTPTR g_syscallHandle[SYS_CALL_NUM] = {0};
- static UINT8 g_syscallNArgs[(SYS_CALL_NUM + 1) / NARG_PER_BYTE] = {0};
-
- void SyscallHandleInit(void)
- {
- #define SYSCALL_HAND_DEF(id, fun, rType, nArg)
- 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宏的对齐格式我做了一点调整。 从g_syscallNArgs成员赋值以及定义的地方,能看出它的每个UINT8成员被用来存放两个系统调用的参数个数,从而实现更少的内存占用; syscall_lookup.h文件和los_syscall.c位于同一目录,它记录了系统调用函数对照表,我们仅节取一部分:
- SYSCALL_HAND_DEF(__NR_read, SysRead, ssize_t, ARG_NUM_3)
- SYSCALL_HAND_DEF(__NR_write, SysWrite, ssize_t, ARG_NUM_3) // <-- 我们要跟踪的 write 在这里
- SYSCALL_HAND_DEF(__NR_open, SysOpen, int, ARG_NUM_7)
- SYSCALL_HAND_DEF(__NR_close, SysClose, int, ARG_NUM_1)
- SYSCALL_HAND_DEF(__NR_creat, SysCreat, int, ARG_NUM_2)
- SYSCALL_HAND_DEF(__NR_unlink, SysUnlink, int, ARG_NUM_1)
-
- #ifdef LOSCFG_KERNEL_DYNLOAD
- SYSCALL_HAND_DEF(__NR_execve, SysExecve, int, ARG_NUM_3)
- #endif
复制代码
看到这里,write系统调用的内核函数终于找到了——SysWrite。 到此,我们已经知道了liteos-a的系统调用机制是如何实现的。
liteos-a内核SysWrite的实现SysWrite函数的实现位于kernel/liteos_a/syscall/fs_syscall.c文件:
- ssize_t SysWrite(int fd, const void *buf, size_t nbytes)
- {
- int ret;
-
- if (nbytes == 0) {
- return 0;
- }
-
- if (!LOS_IsUserAddressRange((vaddr_t)(UINTPTR)buf, nbytes)) {
- return -EFAULT;
- }
-
- /* Process fd convert to system global fd */
- fd = GetAssociatedSystemFd(fd);
-
- ret = write(fd, buf, nbytes); // <-- ??似曾相识??
- if (ret < 0) {
- return -get_errno();
- }
- return ret;
- }
复制代码
它又调用了write?但是这一次是内核空间的write,不再是 musl libc,经过一番搜索,我们可以找到另一个文件third_party/NuttX/fs/vfs/fs_write.c中的write:
- ssize_t write(int fd, FAR const void *buf, size_t nbytes) {
- #if CONFIG_NFILE_DESCRIPTORS > 0
- FAR struct file *filep;
- if ((unsigned int)fd >= CONFIG_NFILE_DESCRIPTORS)
- #endif
- { /* Write to a socket descriptor is equivalent to send with flags == 0 */
- #if defined(LOSCFG_NET_LWIP_SACK)
- FAR const void *bufbak = buf;
- ssize_t ret;
- if (LOS_IsUserAddress((VADDR_T)(uintptr_t)buf)) {
- if (buf != NULL && nbytes > 0) {
- buf = malloc(nbytes);
- if (buf == NULL) { /* 省略 错误处理 代码 */ }
- if (LOS_ArchCopyFromUser((void*)buf, bufbak, nbytes) != 0) {/* 省略 */}
- }
- }
- ret = send(fd, buf, nbytes, 0); // 这个分支是处理socket fd的
- if (buf != bufbak) {
- free((void*)buf);
- }
- return ret;
- #else
- set_errno(EBADF);
- return VFS_ERROR;
- #endif
- }
-
- #if CONFIG_NFILE_DESCRIPTORS > 0
- /* The descriptor is in the right range to be a file descriptor... write
- * to the file.
- */
- if (fd <= STDERR_FILENO && fd >= STDIN_FILENO) { /* fd : [0,2] */
- fd = ConsoleUpdateFd();
- if (fd < 0) {
- set_errno(EBADF);
- return VFS_ERROR;
- }
- }
-
- int ret = fs_getfilep(fd, &filep);
- if (ret < 0) {
- /* The errno value has already been set */
- return VFS_ERROR;
- }
-
- if (filep->f_oflags & O_DIRECTORY) {
- set_errno(EBADF);
- return VFS_ERROR;
- }
-
- if (filep->f_oflags & O_APPEND) {
- if (file_seek64(filep, 0, SEEK_END) == -1) {
- return VFS_ERROR;
- }
- }
-
- /* Perform the write operation using the file descriptor as an index */
- return file_write(filep, buf, nbytes);
- #endif
- }
复制代码
找到这段代码,我们知道了: liteos-a的vfs是在NuttX基础上实现的,NuttX是一个开源RTOS项目; liteos-a的TCP/IP协议栈是基于lwip的,lwip也是一个开源项目; 这段代码中的write分为两个分支,socket fd调用lwip的send,另一个分支调用file_write;
至于,file_write如何调用到存储设备驱动程序,则是更底层的实现了,本文不在继续分析。
补充说明本文内容均是基于鸿蒙系统开源项目OpenHarmony源码静态分析所整理,没有进行实际的运行环境调试,实际执行过程可能有所差异,希望发现错误的读者及时指正。文中所有路径均为整个openharmony源码树上的相对路径(而非liteos源码相对路径)。
参考链接
) {
l10n=1;
nl_type[s[1]-'0'] = INT;
w = nl_arg[s[1]-'0'].i;
s+=3;
} else if (!l10n) {
w = f ? va_arg(*ap, int) : 0;
s++;
} else goto inval;
if (w<0) fl|=LEFT_ADJ, w=-w;
} else if ((w=getint(&s))<0) goto overflow;
/* Read precision */
if (*s=='.' && s[1]=='*') {
if (isdigit(s[2]) && s[3]=='
从注释和代码结构可以看出,这个函数实现了格式化字符串展开的主要流程,这里又调用了out和pad两个函数,从命名猜测应该分别是向内存缓冲区写入内容和填充内容的函数,它们的实现也位于vfprintf.c中: [ DISCUZ_CODE_2857 ]
它们又调用了__fwritex,它的实现位于third_party/musl/src/stdio/fwrite.c: [ DISCUZ_CODE_2858 ]
这里又出现了vfprintf中出现的f->write(f, s, i),下面我们就分析这个函数实际底是什么? 我们先找到它的定义prebuilts/lite/sysroot/usr/include/arm-liteos/bits/alltypes.h: [ DISCUZ_CODE_2859 ]
以及third_party/musl/src/internal/stdio_impl.h: [ DISCUZ_CODE_2860 ]
我们再继续寻找stdout的各个成员值是什么? 可以找到third_party/musl/src/stdio/stdout.c文件中的: [ DISCUZ_CODE_2861 ]
FILE *const stdout = &__stdout_FILE; // <-- stdout 在这里 third_party/musl/src/stdio/__stdout_write.c文件中: [ DISCUZ_CODE_2862 ]
这段代码里调用了SYS_ioctl系统调用,但主体流程是下方的函数__stdio_write,它的实现在third_party/musl/src/stdio/__stdio_write.c文件中: [ DISCUZ_CODE_2863 ]
至此,我们看到了printf函数最终调用到了两个系统调用SYS_ioctl和SYS_write。 musl libc的syscall函数实现分析在上一节中,我们看到printf最终调用到了两个长得像系统调用的函数syscall和__syscall。 系统调用宏syscall的实现在musl代码仓(third_party/musl)下搜索:
$ find . -name '*.h' | xargs grep --color -n 'ssyscall('
./kernel/include/unistd.h:198:long syscall(long, ...);
./src/internal/syscall.h:44:#define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))
./include/unistd.h:199:long syscall(long, ...);
可以找到third_party/musl/src/internal/syscall.h: [ DISCUZ_CODE_2864 ]
这里可以看到它们两者都是宏,而syscall调用了__syscall,而__syscall又调用了__SYSCALL_DISP,它的实现也在同一个文件中: [ DISCUZ_CODE_2865 ]
我们以__stdio_write中调用syscall处进行分析,即尝试展开syscall(SYS_writev, f->fd, iov, iovcnt); [ DISCUZ_CODE_2866 ]
先忽略最外层的 __syscall_ret,展开__SYSCALL_DISP部分: [ DISCUZ_CODE_2867 ]
忽略外层的__SYSCALL_CONCAT,展开__SYSCALL_NARGS_X部分: [ DISCUZ_CODE_2868 ]
回到__SYSCALL_CONCAT 展开流程, [ DISCUZ_CODE_2869 ]
再回到__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)展开流程,结果应该是: [ DISCUZ_CODE_2870 ]
系统调用函数__syscall3的实现这些__syscall[1-7]的系统调用包装宏定义如下: [ DISCUZ_CODE_2871 ]
继续搜索发现有多出匹配,我们关注arch/arm目录下的文件,因为ARM Cortext A7是Armv7-A指令集的32位CPU(如果是Armv8-A指令集的64位CPU则对应arch/aarch64下的文件): [ DISCUZ_CODE_2872 ]
这段代码中还有三个宏,__ASM____R7__、__asm_syscall和R7_OPERAND: [ DISCUZ_CODE_2873 ]
它们有两个实现版,分别对应于编译器THUMB选项的开启和关闭。这两种选项条件下的代码流程基本一致,以下仅以未开启THUMB选项为例进行分析。这两个宏展开后的__syscall3函数内容为: [ DISCUZ_CODE_2874 ]
这里最后的一个内嵌汇编比较复杂,它符合如下格式(具体细节可以查阅gcc内嵌汇编文档的扩展汇编说明): [ DISCUZ_CODE_2875 ]
汇编模板为:"svc 0", 输出参数部分为:"=r"(r0),输出寄存器为r0 输入参数部分为:"r"(r7), "0"(r0), "r"(r1), "r"(r2),输入寄存器为r7,r0,r1,r2,("0"的含义是,这个输入寄存器必须和输出寄存器第0个位置一样) Clobber部分为:"memory" 这里我们只需要记住:系统调用号存放在r7寄存器,参数存放在r0,r1,r2,返回值最终会存放在r0中; SVC指令,ARM Cortex A7手册 的解释为: The SVC instruction causes a Supervisor Call exception. This provides a mechanism for unprivileged software to make a call to the operating system, or other system component that is accessible only at PL1.
翻译过来就是说 SVC指令会触发一个“特权调用”异常。这为非特权软件调用操作系统或其他只能在PL1级别访问的系统组件提供了一种机制。
详细的指令说明在 到这里,我们分析了鸿蒙系统上应用程序如何进入内核态,主要分析的是musl libc的实现。
liteos-a内核的系统调用实现分析既然SVC能够触发一个异常,那么我们就要看看liteos-a内核是如何处理这个异常的。
ARM Cortex A7中断向量表在ARM架构参考手册中,可以找到中断向量表的说明: 可以看到SVC中断向量的便宜地址是0x08,我们可以在kernel/liteos_a/arch/arm/arm/src/startup目录的reset_vector_mp.S文件和reset_vector_up.S文件中找到相关汇编代码: [ DISCUZ_CODE_2876 ]
SVC中断处理函数上面的汇编代码中可以看到,_osExceptSwiHdl函数就是SVC异常处理函数,具体实现在kernel/liteos_a/arch/arm/arm/src/los_hw_exc.S文件中: [ DISCUZ_CODE_2877 ]
这段代码的注释较为清楚,可以看到,内核模式会继续调用OsKernelSVCHandler,用户模式会继续调用OsArmA32SyscallHandle函数;
OsArmA32SyscallHandle函数我们这里分析的流程是从用户模式进入的,所以调用的是OsArmA32SyscallHandle,它的实现位于kernel/liteos_a/syscall/los_syscall.c文件: [ DISCUZ_CODE_2878 ]
这个函数中用到了个全局数组g_syscallHandle和g_syscallNArgs,它们的定义以及初始化函数也在同一个文件中: [ DISCUZ_CODE_2879 ]
其中SYSCALL_HAND_DEF宏的对齐格式我做了一点调整。 从g_syscallNArgs成员赋值以及定义的地方,能看出它的每个UINT8成员被用来存放两个系统调用的参数个数,从而实现更少的内存占用; syscall_lookup.h文件和los_syscall.c位于同一目录,它记录了系统调用函数对照表,我们仅节取一部分: [ DISCUZ_CODE_2880 ] 看到这里,write系统调用的内核函数终于找到了——SysWrite。 到此,我们已经知道了liteos-a的系统调用机制是如何实现的。
liteos-a内核SysWrite的实现SysWrite函数的实现位于kernel/liteos_a/syscall/fs_syscall.c文件: [ DISCUZ_CODE_2881 ]
它又调用了write?但是这一次是内核空间的write,不再是 musl libc,经过一番搜索,我们可以找到另一个文件third_party/NuttX/fs/vfs/fs_write.c中的write: [ DISCUZ_CODE_2882 ]
找到这段代码,我们知道了: liteos-a的vfs是在NuttX基础上实现的,NuttX是一个开源RTOS项目; liteos-a的TCP/IP协议栈是基于lwip的,lwip也是一个开源项目; 这段代码中的write分为两个分支,socket fd调用lwip的send,另一个分支调用file_write;
至于,file_write如何调用到存储设备驱动程序,则是更底层的实现了,本文不在继续分析。
补充说明本文内容均是基于鸿蒙系统开源项目OpenHarmony源码静态分析所整理,没有进行实际的运行环境调试,实际执行过程可能有所差异,希望发现错误的读者及时指正。文中所有路径均为整个openharmony源码树上的相对路径(而非liteos源码相对路径)。
参考链接
) {
nl_type[s[2]-'0'] = INT;
p = nl_arg[s[2]-'0'].i;
s+=4;
} else if (!l10n) {
p = f ? va_arg(*ap, int) : 0;
s+=2;
} else goto inval;
xp = (p>=0);
} else if (*s=='.') {
s++;
p = getint(&s);
xp = 1;
} else {
p = -1;
xp = 0;
}
/* Format specifier state machine */
st=0;
do {
if (OOB(*s)) goto inval;
ps=st;
st=states[st]S(*s++);
} while (st-1
if (!st) goto inval;
/* Check validity of argument type (nl/normal) */
if (st==NOARG) {
if (argpos>=0) goto inval;
} else {
if (argpos>=0) nl_type[argpos]=st, arg=nl_arg[argpos];
else if (f) pop_arg(&arg, st, ap);
else return 0;
}
if (!f) continue;
z = buf + sizeof(buf);
prefix = "-+ 0X0x";
pl = 0;
t = s[-1];
/* Transform ls,lc -> S,C */
if (ps && (t&15)==3) t&=~32;
/* - and 0 flags are mutually exclusive */
if (fl & LEFT_ADJ) fl &= ~ZERO_PAD;
switch(t) {
case 'n':
switch(ps) {
case BARE: *(int *)arg.p = cnt; break;
case LPRE: *(long *)arg.p = cnt; break;
case LLPRE: *(long long *)arg.p = cnt; break;
case HPRE: *(unsigned short *)arg.p = cnt; break;
case HHPRE: *(unsigned char *)arg.p = cnt; break;
case ZTPRE: *(size_t *)arg.p = cnt; break;
case JPRE: *(uintmax_t *)arg.p = cnt; break;
}
continue;
case 'p':
p = MAX(p, 2*sizeof(void*));
t = 'x';
fl |= ALT_FORM;
case 'x': case 'X':
a = fmt_x(arg.i, z, t&32);
if (arg.i && (fl & ALT_FORM)) prefix+=(t>>4), pl=2;
if (0) {
case 'o':
a = fmt_o(arg.i, z);
if ((fl&ALT_FORM) && p
} if (0) {
case 'd': case 'i':
pl=1;
if (arg.i>INTMAX_MAX) {
arg.i=-arg.i;
} else if (fl & MARK_POS) {
prefix++;
} else if (fl & PAD_POS) {
prefix+=2;
} else pl=0;
case 'u':
a = fmt_u(arg.i, z);
}
if (xp && p<0) goto overflow;
if (xp) fl &= ~ZERO_PAD;
if (!arg.i && !p) {
a=z;
break;
}
p = MAX(p, z-a + !arg.i);
break;
case 'c':
*(a=z-(p=1))=arg.i;
fl &= ~ZERO_PAD;
break;
case 'm':
if (1) a = strerror(errno); else
case 's':
a = arg.p ? arg.p : "(null)";
z = a + strnlen(a, p<0 ? INT_MAX : p);
if (p<0 && *z) goto overflow;
p = z-a;
fl &= ~ZERO_PAD;
break;
case 'C':
wc[0] = arg.i;
wc[1] = 0;
arg.p = wc;
p = -1;
case 'S':
ws = arg.p;
for (i=l=0; i =0 && l<=p-i; i+=l);
if (l<0) return -1;
if (i > INT_MAX) goto overflow;
p = i;
pad(f, ' ', w, p, fl);
ws = arg.p;
for (i=0; i<0U+p && *ws && i+(l=wctomb(mb, *ws++))<=p; i+=l)
out(f, mb, l);
pad(f, ' ', w, p, fl^LEFT_ADJ);
l = w>p ? w : p;
continue;
case 'e': case 'f': case 'g': case 'a':
case 'E': case 'F': case 'G': case 'A':
if (xp && p<0) goto overflow;
l = fmt_fp(f, arg.f, w, p, fl, t);
if (l<0) goto overflow;
continue;
}
if (p < z-a) p = z-a;
if (p > INT_MAX-pl) goto overflow;
if (w < pl+p) w = pl+p;
if (w > INT_MAX-cnt) goto overflow;
pad(f, ' ', w, pl+p, fl);
out(f, prefix, pl);
pad(f, '0', w, pl+p, fl^ZERO_PAD);
pad(f, '0', p, z-a, 0);
out(f, a, z-a);
pad(f, ' ', w, pl+p, fl^LEFT_ADJ);
l = w;
}
if (f) return cnt;
if (!l10n) return 0;
for (i=1; i<=NL_ARGMAX && nl_type; i++)
pop_arg(nl_arg+i, nl_type, ap);
for (; i<=NL_ARGMAX && !nl_type; i++);
if (i<=NL_ARGMAX) goto inval;
return 1;
inval: // 删除了错误处理代码
overflow: // 删除了错误处理代码
}[/code]
从注释和代码结构可以看出,这个函数实现了格式化字符串展开的主要流程,这里又调用了out和pad两个函数,从命名猜测应该分别是向内存缓冲区写入内容和填充内容的函数,它们的实现也位于vfprintf.c中: [ DISCUZ_CODE_2857 ]
它们又调用了__fwritex,它的实现位于third_party/musl/src/stdio/fwrite.c: [ DISCUZ_CODE_2858 ]
这里又出现了vfprintf中出现的f->write(f, s, i),下面我们就分析这个函数实际底是什么? 我们先找到它的定义prebuilts/lite/sysroot/usr/include/arm-liteos/bits/alltypes.h: [ DISCUZ_CODE_2859 ]
以及third_party/musl/src/internal/stdio_impl.h: [ DISCUZ_CODE_2860 ]
我们再继续寻找stdout的各个成员值是什么? 可以找到third_party/musl/src/stdio/stdout.c文件中的: [ DISCUZ_CODE_2861 ]
FILE *const stdout = &__stdout_FILE; // <-- stdout 在这里third_party/musl/src/stdio/__stdout_write.c文件中: [ DISCUZ_CODE_2862 ]
这段代码里调用了SYS_ioctl系统调用,但主体流程是下方的函数__stdio_write,它的实现在third_party/musl/src/stdio/__stdio_write.c文件中: [ DISCUZ_CODE_2863 ]
至此,我们看到了printf函数最终调用到了两个系统调用SYS_ioctl和SYS_write。 musl libc的syscall函数实现分析在上一节中,我们看到printf最终调用到了两个长得像系统调用的函数syscall和__syscall。 系统调用宏syscall的实现在musl代码仓(third_party/musl)下搜索:
$ find . -name '*.h' | xargs grep --color -n 'ssyscall('
./kernel/include/unistd.h:198:long syscall(long, ...);
./src/internal/syscall.h:44:#define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))
./include/unistd.h:199:long syscall(long, ...);
可以找到third_party/musl/src/internal/syscall.h: [ DISCUZ_CODE_2864 ]
这里可以看到它们两者都是宏,而syscall调用了__syscall,而__syscall又调用了__SYSCALL_DISP,它的实现也在同一个文件中: [ DISCUZ_CODE_2865 ]
我们以__stdio_write中调用syscall处进行分析,即尝试展开syscall(SYS_writev, f->fd, iov, iovcnt); [ DISCUZ_CODE_2866 ]
先忽略最外层的 __syscall_ret,展开__SYSCALL_DISP部分: [ DISCUZ_CODE_2867 ]
忽略外层的__SYSCALL_CONCAT,展开__SYSCALL_NARGS_X部分: [ DISCUZ_CODE_2868 ]
回到__SYSCALL_CONCAT 展开流程, [ DISCUZ_CODE_2869 ]
再回到__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)展开流程,结果应该是: [ DISCUZ_CODE_2870 ]
系统调用函数__syscall3的实现这些__syscall[1-7]的系统调用包装宏定义如下: [ DISCUZ_CODE_2871 ]
继续搜索发现有多出匹配,我们关注arch/arm目录下的文件,因为ARM Cortext A7是Armv7-A指令集的32位CPU(如果是Armv8-A指令集的64位CPU则对应arch/aarch64下的文件): [ DISCUZ_CODE_2872 ]
这段代码中还有三个宏,__ASM____R7__、__asm_syscall和R7_OPERAND: [ DISCUZ_CODE_2873 ]
它们有两个实现版,分别对应于编译器THUMB选项的开启和关闭。这两种选项条件下的代码流程基本一致,以下仅以未开启THUMB选项为例进行分析。这两个宏展开后的__syscall3函数内容为: [ DISCUZ_CODE_2874 ]
这里最后的一个内嵌汇编比较复杂,它符合如下格式(具体细节可以查阅gcc内嵌汇编文档的扩展汇编说明): [ DISCUZ_CODE_2875 ]
汇编模板为:"svc 0", 输出参数部分为:"=r"(r0),输出寄存器为r0 输入参数部分为:"r"(r7), "0"(r0), "r"(r1), "r"(r2),输入寄存器为r7,r0,r1,r2,("0"的含义是,这个输入寄存器必须和输出寄存器第0个位置一样) Clobber部分为:"memory" 这里我们只需要记住:系统调用号存放在r7寄存器,参数存放在r0,r1,r2,返回值最终会存放在r0中; SVC指令,ARM Cortex A7手册 的解释为: The SVC instruction causes a Supervisor Call exception. This provides a mechanism for unprivileged software to make a call to the operating system, or other system component that is accessible only at PL1.
翻译过来就是说 SVC指令会触发一个“特权调用”异常。这为非特权软件调用操作系统或其他只能在PL1级别访问的系统组件提供了一种机制。
详细的指令说明在 到这里,我们分析了鸿蒙系统上应用程序如何进入内核态,主要分析的是musl libc的实现。
liteos-a内核的系统调用实现分析既然SVC能够触发一个异常,那么我们就要看看liteos-a内核是如何处理这个异常的。
ARM Cortex A7中断向量表在ARM架构参考手册中,可以找到中断向量表的说明: 可以看到SVC中断向量的便宜地址是0x08,我们可以在kernel/liteos_a/arch/arm/arm/src/startup目录的reset_vector_mp.S文件和reset_vector_up.S文件中找到相关汇编代码: [ DISCUZ_CODE_2876 ]
SVC中断处理函数上面的汇编代码中可以看到,_osExceptSwiHdl函数就是SVC异常处理函数,具体实现在kernel/liteos_a/arch/arm/arm/src/los_hw_exc.S文件中: [ DISCUZ_CODE_2877 ]
这段代码的注释较为清楚,可以看到,内核模式会继续调用OsKernelSVCHandler,用户模式会继续调用OsArmA32SyscallHandle函数;
OsArmA32SyscallHandle函数我们这里分析的流程是从用户模式进入的,所以调用的是OsArmA32SyscallHandle,它的实现位于kernel/liteos_a/syscall/los_syscall.c文件: [ DISCUZ_CODE_2878 ]
这个函数中用到了个全局数组g_syscallHandle和g_syscallNArgs,它们的定义以及初始化函数也在同一个文件中: [ DISCUZ_CODE_2879 ]
其中SYSCALL_HAND_DEF宏的对齐格式我做了一点调整。 从g_syscallNArgs成员赋值以及定义的地方,能看出它的每个UINT8成员被用来存放两个系统调用的参数个数,从而实现更少的内存占用; syscall_lookup.h文件和los_syscall.c位于同一目录,它记录了系统调用函数对照表,我们仅节取一部分: [ DISCUZ_CODE_2880 ]看到这里,write系统调用的内核函数终于找到了——SysWrite。 到此,我们已经知道了liteos-a的系统调用机制是如何实现的。
liteos-a内核SysWrite的实现SysWrite函数的实现位于kernel/liteos_a/syscall/fs_syscall.c文件: [ DISCUZ_CODE_2881 ]
它又调用了write?但是这一次是内核空间的write,不再是 musl libc,经过一番搜索,我们可以找到另一个文件third_party/NuttX/fs/vfs/fs_write.c中的write: [ DISCUZ_CODE_2882 ]
找到这段代码,我们知道了: liteos-a的vfs是在NuttX基础上实现的,NuttX是一个开源RTOS项目; liteos-a的TCP/IP协议栈是基于lwip的,lwip也是一个开源项目; 这段代码中的write分为两个分支,socket fd调用lwip的send,另一个分支调用file_write;
至于,file_write如何调用到存储设备驱动程序,则是更底层的实现了,本文不在继续分析。
补充说明本文内容均是基于鸿蒙系统开源项目OpenHarmony源码静态分析所整理,没有进行实际的运行环境调试,实际执行过程可能有所差异,希望发现错误的读者及时指正。文中所有路径均为整个openharmony源码树上的相对路径(而非liteos源码相对路径)。
参考链接
0
|
|
|
|