必答题(需要在实验报告中回答) - hello程序是什么, 它从而何来, 要到哪里去
到此为止, PA中的所有组件已经全部亮相, 整个计算机系统也开始趋于完整. 你也已经在这个自己创造的计算机系统上跑起了hello这个第一个还说得过去的用户程序 (dummy是给大家热身用的, 不算), 好消息是, 我们已经距离运行仙剑奇侠传不远了(下一个阶段就是啦).
不过按照PA的传统, 光是跑起来还是不够的, 你还要明白它究竟怎么跑起来才行. 于是来回答这道必答题吧:
我们知道
navy-apps/tests/hello/hello.c只是一个C源文件, 它会被编译链接成一个ELF文件. 那么, hello程序一开始在哪里? 它是怎么出现内存中的? 为什么会出现在目前的内存位置? 它的第一条指令在哪里? 究竟是怎么执行到它的第一条指令的? hello程序在不断地打印字符串, 每一个字符又是经历了什么才会最终出现在终端上?
上面一口气问了很多问题, 我们想说的是, 这其中蕴含着非常多需要你理解的细节. 我们希望你能够认真整理其中涉及的每一行代码, 然后用自己的语言融会贯通地把这个过程的理解描述清楚, 而不是机械地分点回答这几个问题.
同样地, 上一阶段的必答题"理解穿越时空的旅程"也已经涵盖了一部分内容, 你可以把它的回答包含进来, 但需要描述清楚有差异的地方. 另外, C库中printf()到write()的过程比较繁琐, 而且也不属于PA的主线内容, 这一部分不必展开回答. 而且你也已经在PA2中实现了自己的printf()了, 相信你也不难理解字符串格式化的过程. 如果你对Newlib的实现感兴趣, 你也可以RTFSC.
总之, 扣除C库中printf()到write()转换的部分, 剩下的代码就是你应该理解透彻的了. 于是, 努力去理解每一行代码吧!
答:
要理解hello从何而来,自然是从它的makefile开始。hello的makefile include了navy-apps下的大makefile, 其中
## 4. ISA-Specific Configurations
### Paste in ISA-specific configurations (e.g., from `scripts/x86.mk`)
-include $(NAVY_HOME)/scripts/$(ISA).mk
又include了ISA相关的makefile, 继续找到$(NAVY_HOME)/scripts/riscv32.mk以及$(NAVY_HOME)/scripts/riscv/common.mk可以看到这几行
LNK_ADDR = $(if $(VME), 0x40000000, 0x83000000)
CFLAGS += -fno-pic -march=rv64g -mcmodel=medany
LDFLAGS += --no-relax -Ttext-segment $(LNK_ADDR)
目前还没做到VME, 所以LNK_ADDR为0x83000000,LDFLAGS是传递给链接器(ld)的参数,-Ttext-segment指定程序代码段(.text)运行时的地址。在编译出的elf文件中,可以看到相应的地址:
$ riscv64-linux-gnu-readelf -a build/hello-riscv32
ELF Header: (omitted)
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 830000b4 0000b4 005e00 00 AX 0 0 4
[ 2] .rodata PROGBITS 83005eb4 005eb4 0003fd 00 A 0 0 4
[ 3] .eh_frame PROGBITS 830062b4 0062b4 000fd0 00 A 0 0 4
[ 4] .data PROGBITS 83008000 008000 000830 00 WA 0 0 8
[ 5] .sdata PROGBITS 83008830 008830 000068 00 WA 0 0 4
[ 6] .sbss NOBITS 83008898 008898 000018 00 WA 0 0 4
[ 7] .bss NOBITS 830088b0 008898 000028 00 WA 0 0 4
[ 8] .comment PROGBITS 00000000 008898 000012 01 MS 0 0 1
[ 9] .riscv.attributes RISCV_ATTRIBUTE 00000000 0088aa 000061 00 0 0 1
[10] .symtab SYMTAB 00000000 00890c 001010 10 11 126 4
[11] .strtab STRTAB 00000000 00991c 0007ae 00 0 0 1
[12] .shstrtab STRTAB 00000000 00a0ca 000066 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
RISCV_ATTRIBUT 0x0088aa 0x00000000 0x00000000 0x00061 0x00000 R 0x1
LOAD 0x000000 0x83000000 0x83000000 0x07284 0x07284 R E 0x1000
LOAD 0x008000 0x83008000 0x83008000 0x00898 0x008d8 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
编译出elf以后我把它复制到nanos下,命名为ramdisk.img
cp $NAVY_HOME/tests/hello/build/hello-riscv32 ~/ics2024/nanos-lite/build/ramdisk.img
nanos-like/src/resources.S中把这个ramdisk.img二进制包括进来,最后一起打包到编译出的nanos二进制文件中,并且定义了全局符号ramdisk_start, ramdisk_end用于记录img的起始和末尾地址。
nanos启动时先调用init_irq然后经过一通调用最后在am的cte中执行csrw指令把cte的__am_asm_trap函数地址写入中断向量寄存器。CPU(nemu)执行到syscall指令时即跳转到这个函数,该函数保存上下文后最终会调用nanos注册的do_event函数。
启动过程继续调用init_proc再调用naive_uload, 解析ramdisk.img的elf头,按Program Headers把要执行的二进制复制到0x83000000开始的位置,然后跳转到entry开始执行hello。
hello打印字符串时,调用libc(newlib)提供的printf函数,newlib内部最终会调用libos的syscall.c里面的_write函数,然后调用syscall也即ecall指令,并用寄存器传递参数。如上文所说最终跳转到do_event函数然后调用do_syscall函数,根据syscall number执行对应write操作,调用am提供的putch函数打印到终端。
由上文分析可见,navy-apps与nanos交互的interface是syscall, 相关支持库是newlib和libos. 而nanos和CPU(nemu)交互的interface是ISA, 相关支持库是abstract-machine及其组件。