### 必答题(需要在实验报告中回答) - 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, 其中 ```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`可以看到这几行 ```makefile 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及其组件。