应用程序的组成与编译
我们首先来看一个应用程序,这里我们假定是hello应用程序,在user/hello.c
中实现,代码如下:
#include <stdio.h>
#include <ulib.h>
int main(void) {
cprintf("Hello world!!.\n");
cprintf("I am process %d.\n", getpid());
cprintf("hello pass.\n");
return 0;
}
hello应用程序只是输出一些字符串,并通过系统调用sys_getpid
(在getpid
函数中调用)输出代表hello应用程序执行的用户进程的进程标识--pid
。
首先,我们需要了解ucore操作系统如何能够找到hello应用程序。这需要分析ucore和hello是如何编译的。修改Makefile,把第六行注释掉。然后在本实验源码目录下执行make,可得到如下输出:
+ cc user/hello.c
riscv64-unknown-elf-gcc -Iuser/ -mcmodel=medany -O2 -std=gnu99 -Wno-unused -fno-builtin -Wall -nostdinc -fno-stack-protector -ffunction-sections -fdata-sections -Ilibs/ -Iuser/include/ -Iuser/libs/ -c user/hello.c -o obj/user/hello.o
riscv64-unknown-elf-ld -m elf64lriscv -nostdlib --gc-sections -T tools/user.ld -o obj/__user_hello.out obj/user/libs/panic.o obj/user/libs/syscall.o obj/user/libs/ulib.o obj/user/libs/initcode.o obj/user/libs/stdio.o obj/user/libs/umain.o obj/libs/string.o obj/libs/printfmt.o obj/libs/hash.o obj/libs/rand.o obj/user/hello.o
+ ld bin/kernel
riscv64-unknown-elf-ld -m elf64lriscv -nostdlib --gc-sections -T tools/kernel.ld -o bin/kernel obj/kern/init/entry.o obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/ide.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/trapentry.o obj/kern/mm/vmm.o obj/kern/mm/swap.o obj/kern/mm/kmalloc.o obj/kern/mm/swap_fifo.o obj/kern/mm/default_pmm.o obj/kern/mm/pmm.o obj/kern/fs/swapfs.o obj/kern/process/entry.o obj/kern/process/switch.o obj/kern/process/proc.o obj/kern/schedule/sched.o obj/kern/syscall/syscall.o obj/libs/string.o obj/libs/printfmt.o obj/libs/hash.o obj/libs/rand.o --format=binary obj/__user_hello.out obj/__user_badarg.out obj/__user_forktree.out obj/__user_faultread.out obj/__user_divzero.out obj/__user_exit.out obj/__user_softint.out obj/__user_waitkill.out obj/__user_spin.out obj/__user_yield.out obj/__user_badsegment.out obj/__user_testbss.out obj/__user_faultreadkernel.out obj/__user_forktest.out obj/__user_pgdir.out --format=default
riscv64-unknown-elf-objcopy bin/kernel --strip-all -O binary bin/ucore.img
从中可以看出,hello应用程序不仅仅是hello.c
,还包含了支持hello应用程序的用户态库:
user/libs/initcode.S
:所有应用程序的起始用户态执行地址“_start
”,调整了EBP和ESP后,调用umain
函数。
user/libs/umain.c
:实现了umain
函数,这是所有应用程序执行的第一个C函数,它将调用应用程序的main
函数,并在main
函数结束后调用exit
函数,而exit
函数最终将调用sys_exit
系统调用,让操作系统回收进程资源。
user/libs/ulib.[ch]
:实现了最小的C函数库,除了一些与系统调用无关的函数,其他函数是对访问系统调用的包装。
user/libs/syscall.[ch]
:用户层发出系统调用的具体实现。
user/libs/stdio.c
:实现cprintf
函数,通过系统调用sys_putc
来完成字符输出。
user/libs/panic.c
:实现__panic/__warn
函数,通过系统调用sys_exit
完成用户进程退出。
除了这些用户态库函数实现外,还有一些libs/*.[ch]
是操作系统内核和应用程序共用的函数实现。这些用户库函数其实在本质上与UNIX系统中的标准libc没有区别,只是实现得很简单,但hello应用程序的正确执行离不开这些库函数。
[!NOTE|style:flat]
libs/.[ch]、user/libs/.[ch]、user/*.[ch]的源码中没有任何特权指令。
在make的最后一步执行了一个ld命令,把hello应用程序的执行码obj/__user_hello.out
连接在了ucore kernel
的末尾。且ld命令会在kernel中会把__user_hello.out
的位置和大小记录在全局变量_binary_obj___user_hello_out_start
和_binary_obj___user_hello_out_size
中,这样这个hello用户程序就能够和ucore内核一起被 OpenSBI加载到内存里中,并且通过这两个全局变量定位hello用户程序执行码的起始位置和大小。而到了与文件系统相关的实验后,ucore会提供一个简单的文件系统,那时所有的用户程序就都不再用这种方法进行加载了,而可以用大家熟悉的文件方式进行加载了。
用户进程的虚拟地址空间
在tools/user.ld描述了用户程序的用户虚拟空间的执行入口虚拟地址:
SECTIONS {
/* Load programs at this address: "." means the current address */
. = 0x800020;
在tools/kernel.ld描述了操作系统的内核虚拟空间的起始入口虚拟地址:
BASE_ADDRESS = 0xFFFFFFFFC0200000;
SECTIONS
{
/* Load the kernel at this address: "." means the current address */
. = BASE_ADDRESS;
这样ucore把用户进程的虚拟地址空间分了两块,一块与内核线程一样,是所有用户进程都共享的内核虚拟地址空间,映射到同样的物理内存空间中,这样在物理内存中只需放置一份内核代码,使得用户进程从用户态进入核心态时,内核代码可以统一应对不同的内核程序;另外一块是用户虚拟地址空间,虽然虚拟地址范围一样,但映射到不同且没有交集的物理内存空间中。这样当ucore把用户进程的执行代码(即应用程序的执行代码)和数据(即应用程序的全局变量等)放到用户虚拟地址空间中时,确保了各个进程不会“非法”访问到其他进程的物理内存空间。