接上文 MIT XV6 - 1.1 Lab: Xv6 and Unix utilities - sleep 是怎样练成的?
user/_sleep 是什么?
book-riscv-rev3.pdf 3.8节有对Xv6 binary文件的格式描述
Xv6 binaries are formatted in the widely-used ELF format, defined in (kernel/elf.h). An ELF binary
consists of an ELF header, struct elfhdr (kernel/elf.h:6), followed by a sequence of program
section headers, struct proghdr (kernel/elf.h:25). Each progvhdr describes a section of the
application that must be loaded into memory; xv6 programs have two program section headers:
one for instructions and one for data.
- 让我们验证一下他的文件格式
riscv64-unknown-elf-readelf -h user/_sleep ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # ELF 文件的魔数,用于标识文件类型 Class: ELF64 # 64 位 ELF 文件 Data: 2's complement, little endian # 使用 2 的补码表示数字,小端序存储 Version: 1 (current) # ELF 格式的当前版本 OS/ABI: UNIX - System V # 目标操作系统为 UNIX System V ABI Version: 0 # ABI 版本号 Type: EXEC (Executable file) # 这是一个可执行文件 Machine: RISC-V # 目标架构为 RISC-V Version: 0x1 # ELF 文件版本 Entry point address: 0x64 # 程序入口点的虚拟地址 Start of program headers: 64 (bytes into file) # 程序头表在文件中的偏移量 Start of section headers: 31008 (bytes into file) # 节头表在文件中的偏移量 Flags: 0x5, RVC, double-float ABI # 标志位:RISC-V 压缩指令集,双精度浮点数 ABI Size of this header: 64 (bytes) # ELF 头的大小 Size of program headers: 56 (bytes) # 每个程序头的大小 Number of program headers: 4 # 程序头的数量 Size of section headers: 64 (bytes) # 每个节头的大小 Number of section headers: 18 # 节头的数量 Section header string table index: 17 # 节头字符串表的索引
我们知道,在Unix系统中,一个新进程的诞生是通过fork和exec配合得来的,通过fork创建一个副本,然后通过exec将指定的binary加载进内存空间中并开始执行,教材中也对此有所讲解,依然是book-riscv-rev3.pdf 3.8节,简单看一眼
-
让我们看看我们的 user/_sleep
riscv64-unknown-elf-objdump -p user/_sleep user/_sleep: file format elf64-littleriscv Program Header: 0x70000003 off 0x0000000000006af8 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**0 filesz 0x000000000000005a memsz 0x0000000000000000 flags r-- LOAD off 0x0000000000001000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**12 filesz 0x0000000000001000 memsz 0x0000000000001000 flags r-x LOAD off 0x0000000000002000 vaddr 0x0000000000001000 paddr 0x0000000000001000 align 2**12 filesz 0x0000000000000000 memsz 0x0000000000000020 flags rw- STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4 filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
-
让我们来看看AI是怎么解释的
-
当然我们的 user/_sleep 里面还有更多东西
riscv64-unknown-elf-objdump -h user/_sleep Sections: Idx Name Size VMA LMA File off Algn # 列标题:索引、名称、大小、虚拟地址、加载地址、文件偏移、对齐 0 .text 0000082a 0000000000000000 0000000000000000 00001000 2**1 # 代码段:包含可执行指令 CONTENTS, ALLOC, LOAD, READONLY, CODE # 属性:有内容、已分配、可加载、只读、代码 1 .rodata 000007d0 0000000000000830 0000000000000830 00001830 2**3 # 只读数据段:包含常量数据(如字符串常量) CONTENTS, ALLOC, LOAD, READONLY, DATA # 属性:有内容、已分配、可加载、只读、数据 2 .data 00000000 0000000000001000 0000000000001000 00002000 2**0 # 数据段:包含已初始化的全局变量 CONTENTS, ALLOC, LOAD, DATA # 属性:有内容、已分配、可加载、数据 3 .bss 00000020 0000000000001000 0000000000001000 00002000 2**3 # BSS段:包含未初始化的全局变量 ALLOC # 属性:已分配 4 .debug_info 00000ff8 0000000000000000 0000000000000000 00002000 2**0 # 调试信息段:包含DWARF调试信息 CONTENTS, READONLY, DEBUGGING, OCTETS # 属性:有内容、只读、调试用、字节数据 5 .debug_abbrev 00000643 0000000000000000 0000000000000000 00002ff8 2**0 # 调试缩写表:DWARF调试信息的缩写表 CONTENTS, READONLY, DEBUGGING, OCTETS # 属性:有内容、只读、调试用、字节数据 6 .debug_loc 00001f21 0000000000000000 0000000000000000 0000363b 2**0 # 调试位置信息:变量位置信息 CONTENTS, READONLY, DEBUGGING, OCTETS # 属性:有内容、只读、调试用、字节数据 7 .debug_aranges 000000f0 0000000000000000 0000000000000000 00005560 2**4 # 调试地址范围:用于快速定位调试信息 CONTENTS, READONLY, DEBUGGING, OCTETS # 属性:有内容、只读、调试用、字节数据 8 .debug_line 000011ab 0000000000000000 0000000000000000 00005650 2**0 # 调试行号信息:源代码行号映射 CONTENTS, READONLY, DEBUGGING, OCTETS # 属性:有内容、只读、调试用、字节数据 9 .debug_str 000002e4 0000000000000000 0000000000000000 000067fb 2**0 # 调试字符串表:调试信息中的字符串 CONTENTS, READONLY, DEBUGGING, OCTETS # 属性:有内容、只读、调试用、字节数据 10 .comment 00000019 0000000000000000 0000000000000000 00006adf 2**0 # 注释段:包含编译器版本等信息 CONTENTS, READONLY # 属性:有内容、只读 11 .riscv.attributes 0000005a 0000000000000000 0000000000000000 00006af8 2**0 # RISC-V特定属性段 CONTENTS, READONLY # 属性:有内容、只读 12 .debug_frame 000004e0 0000000000000000 0000000000000000 00006b58 2**3 # 调试帧信息:用于栈回溯 CONTENTS, READONLY, DEBUGGING, OCTETS # 属性:有内容、只读、调试用、字节数据 13 .debug_ranges 00000050 0000000000000000 0000000000000000 00007038 2**0 # 调试范围信息:用于描述变量范围 CONTENTS, READONLY, DEBUGGING, OCTETS # 属性:有内容、只读、调试用、字节数据
我就不解释了,这个东西给我一天也解释不清楚,解释不完全(是真的不清楚)
user/_sleep 做什么?
让我们打开 user/sleep.asm 看一下汇编代码
0: 1141 addi sp,sp,-16
2: e406 sd ra,8(sp)
4: e022 sd s0,0(sp)
6: 0800 addi s0,sp,16
if (argc != 2)
8: 4789 li a5,2
a: 02f50063 beq a0,a5,2a <main+0x2a>
{
fprintf(2, "Usage: sleep <seconds>\n");
e: 00001597 auipc a1,0x1
12: 82258593 addi a1,a1,-2014 # 830 <malloc+0x100>
16: 853e mv a0,a5
18: 00000097 auipc ra,0x0
1c: 62e080e7 jalr 1582(ra) # 646 <fprintf>
exit(1);
20: 4505 li a0,1
22: 00000097 auipc ra,0x0
26: 2fc080e7 jalr 764(ra) # 31e <exit>
}
if (sleep(atoi(argv[1])) != 0)
2a: 6588 ld a0,8(a1)
2c: 00000097 auipc ra,0x0
30: 1ec080e7 jalr 492(ra) # 218 <atoi>
34: 00000097 auipc ra,0x0
38: 37a080e7 jalr 890(ra) # 3ae <sleep>
3c: cd19 beqz a0,5a <main+0x5a>
{
fprintf(2, "sleep: failed to sleep\n");
3e: 00001597 auipc a1,0x1
42: 80a58593 addi a1,a1,-2038 # 848 <malloc+0x118>
46: 4509 li a0,2
48: 00000097 auipc ra,0x0
4c: 5fe080e7 jalr 1534(ra) # 646 <fprintf>
exit(1);
50: 4505 li a0,1
52: 00000097 auipc ra,0x0
56: 2cc080e7 jalr 716(ra) # 31e <exit>
}
exit(0);
5a: 4501 li a0,0
5c: 00000097 auipc ra,0x0
60: 2c2080e7 jalr 706(ra) # 31e <exit>
注意这一条指令
38: 37a080e7 jalr 890(ra) # 3ae <sleep>
这是一条跳转指令,地址是 3ae,经过搜索可以看到 sleep 函数的汇编代码
00000000000003ae <sleep>: # 函数入口点,地址为 0x3ae
.global sleep # 声明 sleep 为全局符号
sleep: # 函数标签
li a7, SYS_sleep # 将系统调用号 13 (SYS_sleep) 加载到寄存器 a7
3ae: 48b5 li a7,13 # 机器码:48b5 表示 li a7,13
ecall # 执行系统调用指令
3b0: 00000073 ecall # 机器码:00000073 表示 ecall
ret # 从函数返回
3b4: 8082 ret # 机器码:8082 表示 ret
我们忽略其他各种分支判断以及错误码打印,exit调用等等, 可以看到sleep.c的核心就是在条件达成时,调用函数sleep,而 sleep函数内的实现就是利用 ecall 指令触发系统调用,当系统调用完成后,函数返回。
瞎谈
有大佬说过 Algorithms + Data Structures = Programs
那么从操作系统的角度来看,是不是 Data + SysCall = Programs 也是成立的?因为程序的一切的一切最终都是要把你想做的事情组织成一条条数据,通过系统调用的方式来达成目的?比如播放音乐、播放视频、玩video game,毕竟系统调用应该是应用程序控制硬件资源的唯一途径(吧?)。