计算机系统
大作业
计算机科学与技术学院
2024年5月
HelloWorld这个简单的程序蕴含着计算机系统的许多奇妙的知识、神奇的机制,尽管看上去简单,但它是无数程序们的天才的创造、思想的结晶。本文通过逐步分析hello C源代码的预处理、编译、汇编、链接生成可执行程序hello的过程,以及Hello的进程管理、存储管理、IO管理,展示了其中的流程机制,回顾了计算机系统中的所学知识。
关键词:计算机系统,编译系统,进程管理,存储管理,IO管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P(From Program to Process):
编写完成的hello.c文件(program),首先要经过预处理器cpp生成hello.i文件,此时仍为文本文件;再经过编译器ccl编译生成汇编文件hello.s,此时为机器级语言程序;再经过汇编器处理成一个可重定位目标文件hello.o,此时为二进制文件;最后hello.o以及其他可重定位目标文件在链接器ld作用下生成了可执行目标程序hello。接下来用户通过shell输入./hello命令开始执行程序,shell通过fork函数来创建它的子进程(process),再由子进程执行execve函数来加载hello。
图1-1 hello.c执行流程
020(From Zero-0 to Zero-0):
指最初内存并无hello文件的相关内容,shell用execve函数启动hello程序,把虚拟内存对应到物理内存,并从程序入口开始加载和运行,进入main函数执行目标代码,程序结束后,shell父进程回收hello进程,内核删除hello文件相关的数据结构。
1.2 环境与工具
硬件环境:
图1-2 硬件信息
软件环境:Windows 11 64位;Vmware;Ubuntu 20.04
开发与调试工具:Visual Studio Code;vim objdump gdb gcc readelf等工具
1.3 中间结果
hello.i 预处理后得到的文本文件
hello.s 编译后得到的汇编语言文件
hello.o 汇编后得到的可重定位目标文件
hello.elf 用readelf读取hello.o的ELF格式信息
hello_o.asm 反汇编hello.o得到的反汇编文件
hello_out.elf 用readelf读取hello的ELF格式信息
hello.asm 反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章首先介绍了hello的P2P,020流程,包括流程的设计思路和实现方法。然后详细说明了本实验所需的硬件配置、软件平台、开发工具以及本实验生成的各个中间结果文件的名称和功能。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:在源程序被编译器处理之前,cpp根据源文件中的宏定义、条件编译等命令对源文件作以修改。预处理过程中并不直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换。
作用:
- 文件包含:将源文件中以“include”所指文件内容包含到当前文件中
- 宏替换:用实际值替换以“define”定义的内容
- 条件编译:根据“#if”“#endif”等行为进行编译时有选择的挑选
- 布局控制:“#pragma”设定编译器状态或指示编译器完成一些特定工作
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
图2-1 生成hello.i
2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,对比源程序和预处理后的程序。发现预处理指令被扩展成了几千行,源程序的所有注释都被删除了,原本的头文件全都被替换为了其对应的文件内容。
以 stdio.h 为例,预处理过程中,#include指令的作用是把指定的头文件的内容包含到源文件中。stdio.h是标准输入输出库的头文件,它包含了用于读写文件、标准输入输出的函数原型和宏定义等内容。
当预处理器遇到#include<stdio.h>时,它会在系统的头文件路径下查找stdio.h文件,一般在/usr/include目录下,然后把stdio.h文件中的内容复制到源文件中。stdio.h文件中可能还有其他的#include指令,比如#include<stddef.h>或#include<features.h>等,这些头文件也会被递归地展开到源文件中。
预处理器不会对头文件中的内容做任何计算或转换,只是简单地复制和替换。
图2-2 hello.i节选
2.4 本章小结
本章讲述了在Linux环境中,如何用命令对C语言程序进行预处理,以及预处理的含义和作用。然后用一个简单的hello程序演示了从hello.c到hello.i的过程,并分析了预处理后的结果。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:将用高级程序设计语言书写的源程序翻译成等价的汇编语言格式的过程,此处编译是指从hello.i到hello.s即预处理后的文件转化为汇编语言程序。
作用:使高级语言源程序变为汇编语言,提高编程效率和可移植性。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
图3-1 生成hello.s
3.3 Hello的编译结果解析
3.3.1 汇编初始部分
在main函数前有一部分字段展示了节名称
- .file "hello.c"
- .text
- .section .rodata
- .align 8
- .LC0:
- .string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \346\211\213\346\234\272\345\217\267 \347\247\222\346\225\260\357\274\201"
- .LC1:
- .string "Hello %s %s %s\n"
- .text
- .globl main
- .type main, @function
其对应含义如下:
.file 源文件
.text 代码段
.section .rodata 只读数据段
.align 对指令或者数据的存放地址进行对齐的方式
.string 一个字符串
.global 全局变量
.type 一个符号的类型
3.3.2 数据部分
(1)常量
如以下部分的0即为立即数常量,记录为$0
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
(2)局部变量
如下代码将立即数0存到%rbp-4对应的栈地址中,结合hello.c可知,此处的操作是为局部变量i赋值0
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
(3)全局变量
如下代码,本程序中全局变量只有一个,即main函数
- .globl main
- .type main, @function
3.3.3 赋值部分
如下代码,由于i为int型变量,所以使用movl传递双字来实现
- movl $0, -4(%rbp)
3.3.4 算术部分
如下代码,在hello.c中for循环每次结束后i++,该操作体现在汇编代码中的add,因为i为int型变量,所以使用addl指令
- addl $1, -4(%rbp)
3.3.5 关系部分
(1)”!=”关系
如下代码,为hello.c中的条件判断语句if(argc!=5),使用了cmp指令来比较立即数5和参数argc的大小,并且设置了条件码,如果相等则跳转到.L2,否则继续执行后面的语句
- cmpl $5, -20(%rbp)
- je .L2
(2)”<”关系
如下代码,为hello.c中的for(i=0;i<10;i++),同上,通过cmp指令来设置条件码并判断跳转到什么位置
- cmpl $9, -4(%rbp)
- jle .L4
3.3.6 控制转移部分
(1)if判断
如下代码,判断argc是否为5,如果不为5则执行if语言,否则执行其他语句
- cmpl $5, -20(%rbp)
- je .L2
(2)for循环
在hello.s文件中,对于for语句的翻译,采用jmp等跳转语句来实现,当为满足i<=9循环的条件时,使用jle语句,跳转到循环的开始进行新一轮的迭代。
- cmpl $9, -4(%rbp)
- jle .L4
3.3.7 函数操作部分
(1)main函数
参数传递:参数为int argc,char *argv[]
参数argc是main函数的第一个参数,被存放在%rdi中,由代码可见%edi中存的值被压入栈中
- movl %edi, -20(%rbp)
而这行代码可知该值与立即数5判断大小
- cmpl $5, -20(%rbp)
参数*argv[]是main函数的第二个参数,数组中的每个元素都是一个指向char类型的指针
函数调用:通过call指令来进行函数调用,并将要调用的函数地址写入栈中,然后自动跳转到这个调用函数内部
局部变量:使用了局部变量i用于for循环
- printf函数
参数传递:参数为argv[1],argv[2],argv[3]
函数调用:该函数被调用了两次,将字符串起始地址存到%rdi中
- leaq .LC1(%rip), %rax
- movq %rax, %rdi
而如下代码则是分别使用%rcx、%rdx、%rsi来存argv[3],argv[2],argv[1]
- movq -32(%rbp), %rax
- addq $24, %rax
- movq (%rax), %rcx
- movq -32(%rbp), %rax
- addq $16, %rax
- movq (%rax), %rdx
- movq -32(%rbp), %rax
- addq $8, %rax
- movq (%rax), %rax
- movq %rax, %rsi
- atoi函数
参数传递:将argv[4]存到%rdi中
函数调用:使用call指令调用函数atoi
- movq -32(%rbp), %rax
- addq $32, %rax
- movq (%rax), %rax
- movq %rax, %rdi
- call atoi@PLT
- sleep函数
参数传递:将atoi(argv[4])存到%edi中
函数调用:使用call指令调用函数sleep
- movl %eax, %edi
- call sleep@PLT
- getchar函数
无参数传递,直接使用call指令调用函数getchar
- call getchar@PLT
3.4 本章小结
这一章介绍了C编译器如何把hello.i文件转换成hello.s文件的过程,简要说明了编译的含义和功能,演示了编译的指令,并通过分析生成的hello.s文件中的汇编代码,探讨了数据处理,函数调用,赋值、算术、关系等运算以及控制跳转等方面,比较了源代码和汇编代码分别是怎样实现这些操作的。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指汇编器(as)将包含汇编语言的.s文件翻译成机器语言指令,并把这些指令打包成一个可重定位目标文件,即.o文件,.o文件是一个二进制文件,包含main函数的指令编码。
作用:汇编就是将高级语言转化为机器可直接识别的代码文件,将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标文件。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
图4-1 生成hello.o
4.3 可重定位目标elf格式
输入readelf -a hello.o > hello.elf命令来或得hello.elf文件
图4-2 生成hello.elf
图4-3 典型的ELF可重定位目标文件
4.3.1 ELF头(ELF header)
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,如ELF头的大小,目标文件的类型、机器类型、节头部表的文件偏移、节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的。其中目标文件中每个节都有一个固定大小的条目(entry)。
如下为ELF头:
- ELF Header:
- Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
- Class: ELF64
- Data: 2's complement, little endian
- Version: 1 (current)
- OS/ABI: UNIX - System V
- ABI Version: 0
- Type: REL (Relocatable file)
- Machine: Advanced Micro Devices X86-64
- Version: 0x1
- Entry point address: 0x0
- Start of program headers: 0 (bytes into file)
- Start of section headers: 1088 (bytes into file)
- Flags: 0x0
- Size of this header: 64 (bytes)
- Size of program headers: 0 (bytes)
- Number of program headers: 0
- Size of section headers: 64 (bytes)
- Number of section headers: 14
- Section header string table index: 13
4.3.2 节头(section header)
记录各节的name、type、address、offset、size、entsize、flags、link、info、align
如下为节头:
- Section Headers:
- [Nr] Name Type Address Offset
- Size EntSize Flags Link Info Align
- [ 0] NULL 0000000000000000 00000000
- 0000000000000000 0000000000000000 0 0 0
- [ 1] .text PROGBITS 0000000000000000 00000040
- 00000000000000a3 0000000000000000 AX 0 0 1
- [ 2] .rela.text RELA 0000000000000000 000002f0
- 00000000000000c0 0000000000000018 I 11 1 8
- [ 3] .data PROGBITS 0000000000000000 000000e3
- 0000000000000000 0000000000000000 WA 0 0 1
- [ 4] .bss NOBITS 0000000000000000 000000e3
- 0000000000000000 0000000000000000 WA 0 0 1
- [ 5] .rodata PROGBITS 0000000000000000 000000e8
- 0000000000000040 0000000000000000 A 0 0 8
- [ 6] .comment PROGBITS 0000000000000000 00000128
- 000000000000002c 0000000000000001 MS 0 0 1
- [ 7] .note.GNU-stack PROGBITS 0000000000000000 00000154
- 0000000000000000 0000000000000000 0 0 1
- [ 8] .note.gnu.pr[...] NOTE 0000000000000000 00000158
- 0000000000000020 0000000000000000 A 0 0 8
- [ 9] .eh_frame PROGBITS 0000000000000000 00000178
- 0000000000000038 0000000000000000 A 0 0 8
- [10] .rela.eh_frame RELA 0000000000000000 000003b0
- 0000000000000018 0000000000000018 I 11 9 8
- [11] .symtab SYMTAB 0000000000000000 000001b0
- 0000000000000108 0000000000000018 12 4 8
- [12] .strtab STRTAB 0000000000000000 000002b8
- 0000000000000032 0000000000000000 0 0 1
- [13] .shstrtab STRTAB 0000000000000000 000003c8
- 0000000000000074 0000000000000000 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), l (large), p (processor specific)
4.3.3 重定位节
.rel.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般来说,任何调用外部函数或者引用全局变量的指令都需要修改,而调用本地函数的指令不需要修改。可执行目标文件中不包含重定位信息
如下为需要重定位的内容:
- Relocation section '.rela.text' at offset 0x2f0 contains 8 entries:
- Offset Info Type Sym. Value Sym. Name + Addend
- 00000000001c 000300000002 R_X86_64_PC32 0000000000000000 .rodata - 4
- 000000000024 000500000004 R_X86_64_PLT32 0000000000000000 puts - 4
- 00000000002e 000600000004 R_X86_64_PLT32 0000000000000000 exit - 4
- 000000000062 000300000002 R_X86_64_PC32 0000000000000000 .rodata + 2c
- 00000000006f 000700000004 R_X86_64_PLT32 0000000000000000 printf - 4
- 000000000082 000800000004 R_X86_64_PLT32 0000000000000000 atoi - 4
- 000000000089 000900000004 R_X86_64_PLT32 0000000000000000 sleep - 4
- 000000000098 000a00000004 R_X86_64_PLT32 0000000000000000 getchar - 4
- Relocation section '.rela.eh_frame' at offset 0x3b0 contains 1 entry:
- Offset Info Type Sym. Value Sym. Name + Addend
- 000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
- No processor specific unwind information to decode
4.3.4 符号表
.symtab节中包含ELF符号表,符号表中包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息,符号表不包含局部变量的信息
如下为符号表的内容:
- Symbol table '.symtab' contains 11 entries:
- Num: Value Size Type Bind Vis Ndx Name
- 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
- 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
- 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
- 3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata
- 4: 0000000000000000 163 FUNC GLOBAL DEFAULT 1 main
- 5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
- 6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
- 7: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
- 8: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi
- 9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
- 10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar
4.4 Hello.o的结果解析
4.4.1 反汇编
输入objdump -d -r hello.o > hello_o.asm 指令来得到hello.o的反汇编文件
图4-4 生成hello_o.asm
(1)增加机器语言
每一条指令增加了一个十六进制的表示,即该指令的机器语言,如hello.s的一个addq指令表示为:
- addq $24, %rax
而在hello_o.asm中表示为:
- 3f: 48 83 c0 18 add $0x18,%rax
(2)操作数进制
反汇编文件中的所有操作数都改为十六进制,如(1)中的例子,立即数由$24变为了$0x18。
(3)分支转移
反汇编文件中的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量,而不再是hello.s中的短名称(如.L1),jmp指令表示为:
- 39: eb 56 jmp 91 <main+0x91>
(4)函数调用
反汇编文件中对函数的调用与重定位条目相对应,在call后面不再是函数名称,而是下一条指令的地址,这是因为可重定位目标程序尚未经过链接器的处理,调用的函数并不实际存在于实际程序中,需要进一步调用共享库利用链接器才可以具体实现。对于不确定地址的函数调用,在编译过程中计算机会将地址全部设置为0,即指向当前指令的下一条指令,并将该函数名加入到符号表中,并在.rel.text中添加重定位条目。如下代码所示:
- e: e8 00 00 00 00 call 73 <main+0x73>
4.5 本章小结
这一章介绍了汇编的含义和功能。以hello.s文件为例,说明了如何把它汇编成hello.o文件,并生成ELF格式的文件hello.elf。将可重定位目标文件改为ELF格式观察文件内容,对文件中的每个节进行简单解析。通过分析hello.o的反汇编代码(保存在hello_o.asm中)和hello.s的区别和相同点,清楚地理解了汇编语言到机器语言的转换过程,以及为链接而做的准备工作。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:将各种代码和数据片段集合到一个单一文件的过程,这个文件可以被加载到内存执行。链接可以在执行与编译时,即在源代码被翻译成机器代码时;也可以在执行与加载时,即程序被加载器加载到内存并执行时;也可以在运行时。
作用:我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小的、更好管理的模块,可以独立地修改和编译这些模块。当改变这些模块中的一个时,只需简单地重新编译并重新链接。
5.2 在Ubuntu下链接的命令
链接命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
图5-1 生成hello
5.3 可执行目标文件hello的格式
(1)ELF头
hello_out.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以描述了生成该文件的系统的字的大小和字节顺序的16字节序列Magic开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息,与hello.elf相比,hello_out.elf中的基本信息未发生改变,而类型发生改变。程序头大小和节头数量增加,并且有入口地址,如下代码所示:
- ELF Header:
- Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
- Class: ELF64
- Data: 2's complement, little endian
- Version: 1 (current)
- OS/ABI: UNIX - System V
- ABI Version: 0
- Type: EXEC (Executable file)
- Machine: Advanced Micro Devices X86-64
- Version: 0x1
- Entry point address: 0x4010f0
- Start of program headers: 64 (bytes into file)
- Start of section headers: 13560 (bytes into file)
- Flags: 0x0
- Size of this header: 64 (bytes)
- Size of program headers: 56 (bytes)
- Number of program headers: 12
- Size of section headers: 64 (bytes)
- Number of section headers: 27
- Section header string table index: 26
(2)节头
描述了各个节的属性,在链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量来重新设置各个符号的地址,如下代码所示:
- Section Headers:
- [Nr] Name Type Address Offset
- Size EntSize Flags Link Info Align
- [ 0] NULL 0000000000000000 00000000
- 0000000000000000 0000000000000000 0 0 0
- [ 1] .interp PROGBITS 00000000004002e0 000002e0
- 000000000000001c 0000000000000000 A 0 0 1
- [ 2] .note.gnu.pr[...] NOTE 0000000000400300 00000300
- 0000000000000030 0000000000000000 A 0 0 8
- [ 3] .note.ABI-tag NOTE 0000000000400330 00000330
- 0000000000000020 0000000000000000 A 0 0 4
- [ 4] .hash HASH 0000000000400350 00000350
- 0000000000000038 0000000000000004 A 6 0 8
- [ 5] .gnu.hash GNU_HASH 0000000000400388 00000388
- 000000000000001c 0000000000000000 A 6 0 8
- [ 6] .dynsym DYNSYM 00000000004003a8 000003a8
- 00000000000000d8 0000000000000018 A 7 1 8
- [ 7] .dynstr STRTAB 0000000000400480 00000480
- 0000000000000067 0000000000000000 A 0 0 1
- [ 8] .gnu.version VERSYM 00000000004004e8 000004e8
- 0000000000000012 0000000000000002 A 6 0 2
- [ 9] .gnu.version_r VERNEED 0000000000400500 00000500
- 0000000000000030 0000000000000000 A 7 1 8
- [10] .rela.dyn RELA 0000000000400530 00000530
- 0000000000000030 0000000000000018 A 6 0 8
- [11] .rela.plt RELA 0000000000400560 00000560
- 0000000000000090 0000000000000018 AI 6 21 8
- [12] .init PROGBITS 0000000000401000 00001000
- 000000000000001b 0000000000000000 AX 0 0 4
- [13] .plt PROGBITS 0000000000401020 00001020
- 0000000000000070 0000000000000010 AX 0 0 16
- [14] .plt.sec PROGBITS 0000000000401090 00001090
- 0000000000000060 0000000000000010 AX 0 0 16
- [15] .text PROGBITS 00000000004010f0 000010f0
- 00000000000000d8 0000000000000000 AX 0 0 16
- [16] .fini PROGBITS 00000000004011c8 000011c8
- 000000000000000d 0000000000000000 AX 0 0 4
- [17] .rodata PROGBITS 0000000000402000 00002000
- 0000000000000048 0000000000000000 A 0 0 8
- [18] .eh_frame PROGBITS 0000000000402048 00002048
- 00000000000000a0 0000000000000000 A 0 0 8
- [19] .dynamic DYNAMIC 0000000000403e50 00002e50
- 00000000000001a0 0000000000000010 WA 7 0 8
- [20] .got PROGBITS 0000000000403ff0 00002ff0
- 0000000000000010 0000000000000008 WA 0 0 8
- [21] .got.plt PROGBITS 0000000000404000 00003000
- 0000000000000048 0000000000000008 WA 0 0 8
- [22] .data PROGBITS 0000000000404048 00003048
- 0000000000000004 0000000000000000 WA 0 0 1
- [23] .comment PROGBITS 0000000000000000 0000304c
- 000000000000002b 0000000000000001 MS 0 0 1
- [24] .symtab SYMTAB 0000000000000000 00003078
- 0000000000000270 0000000000000018 25 7 8
- [25] .strtab STRTAB 0000000000000000 000032e8
- 000000000000012e 0000000000000000 0 0 1
- [26] .shstrtab STRTAB 0000000000000000 00003416
- 00000000000000e1 0000000000000000 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), l (large), p (processor specific)
(3)程序头
程序头部分是一个结构数组,描述了系统准备程序执行所需的段以及其他信息,如下代码所示:
- Program Headers:
- Type Offset VirtAddr PhysAddr
- FileSiz MemSiz Flags Align
- PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
- 0x00000000000002a0 0x00000000000002a0 R 0x8
- INTERP 0x00000000000002e0 0x00000000004002e0 0x00000000004002e0
- 0x000000000000001c 0x000000000000001c R 0x1
- [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
- LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
- 0x00000000000005f0 0x00000000000005f0 R 0x1000
- LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
- 0x00000000000001d5 0x00000000000001d5 R E 0x1000
- LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
- 0x00000000000000e8 0x00000000000000e8 R 0x1000
- LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
- 0x00000000000001fc 0x00000000000001fc RW 0x1000
- DYNAMIC 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
- 0x00000000000001a0 0x00000000000001a0 RW 0x8
- NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300
- 0x0000000000000030 0x0000000000000030 R 0x8
- NOTE 0x0000000000000330 0x0000000000400330 0x0000000000400330
- 0x0000000000000020 0x0000000000000020 R 0x4
- GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300
- 0x0000000000000030 0x0000000000000030 R 0x8
- GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
- 0x0000000000000000 0x0000000000000000 RW 0x10
- GNU_RELRO 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
- 0x00000000000001b0 0x00000000000001b0 R 0x1
- Dynamic section
如下代码所示:
- Dynamic section at offset 0x2e50 contains 21 entries:
- Tag Type Name/Value
- 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
- 0x000000000000000c (INIT) 0x401000
- 0x000000000000000d (FINI) 0x4011c8
- 0x0000000000000004 (HASH) 0x400350
- 0x000000006ffffef5 (GNU_HASH) 0x400388
- 0x0000000000000005 (STRTAB) 0x400480
- 0x0000000000000006 (SYMTAB) 0x4003a8
- 0x000000000000000a (STRSZ) 103 (bytes)
- 0x000000000000000b (SYMENT) 24 (bytes)
- 0x0000000000000015 (DEBUG) 0x0
- 0x0000000000000003 (PLTGOT) 0x404000
- 0x0000000000000002 (PLTRELSZ) 144 (bytes)
- 0x0000000000000014 (PLTREL) RELA
- 0x0000000000000017 (JMPREL) 0x400560
- 0x0000000000000007 (RELA) 0x400530
- 0x0000000000000008 (RELASZ) 48 (bytes)
- 0x0000000000000009 (RELAENT) 24 (bytes)
- 0x000000006ffffffe (VERNEED) 0x400500
- 0x000000006fffffff (VERNEEDNUM) 1
- 0x000000006ffffff0 (VERSYM) 0x4004e8
- 0x0000000000000000 (NULL) 0x0
- Symbol table
符号表中存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。如下代码所示:
- Symbol table '.dynsym' contains 9 entries:
- Num: Value Size Type Bind Vis Ndx Name
- 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
- 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _[...]@GLIBC_2.34 (2)
- 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (3)
- 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND [...]@GLIBC_2.2.5 (3)
- 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND [...]@GLIBC_2.2.5 (3)
- 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
- 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5 (3)
- 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (3)
- 8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (3)
- Symbol table '.symtab' contains 26 entries:
- Num: Value Size Type Bind Vis Ndx Name
- 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
- 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS crt1.o
- 2: 0000000000400330 32 OBJECT LOCAL DEFAULT 3 __abi_tag
- 3: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
- 4: 0000000000000000 0 FILE LOCAL DEFAULT ABS
- 5: 0000000000403e50 0 OBJECT LOCAL DEFAULT 19 _DYNAMIC
- 6: 0000000000404000 0 OBJECT LOCAL DEFAULT 21 _GLOBAL_OFFSET_TABLE_
- 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_mai[...]
- 8: 0000000000404048 0 NOTYPE WEAK DEFAULT 22 data_start
- 9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5
- 10: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 _edata
- 11: 00000000004011c8 0 FUNC GLOBAL HIDDEN 16 _fini
- 12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5
- 13: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 22 __data_start
- 14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@GLIBC_2.2.5
- 15: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
- 16: 0000000000402000 4 OBJECT GLOBAL DEFAULT 17 _IO_stdin_used
- 17: 0000000000404050 0 NOTYPE GLOBAL DEFAULT 22 _end
- 18: 0000000000401120 5 FUNC GLOBAL HIDDEN 15 _dl_relocate_sta[...]
- 19: 00000000004010f0 38 FUNC GLOBAL DEFAULT 15 _start
- 20: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 __bss_start
- 21: 0000000000401125 163 FUNC GLOBAL DEFAULT 15 main
- 22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5
- 23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5
- 24: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5
- 25: 0000000000401000 0 FUNC GLOBAL HIDDEN 12 _init
- Histogram for bucket list length (total of 3 buckets):
- Length Number % of total Coverage
- 0 0 ( 0.0%)
- 1 0 ( 0.0%) 0.0%
- 2 1 ( 33.3%) 25.0%
- 3 2 ( 66.7%) 100.0%
- Version symbols section '.gnu.version' contains 9 entries:
- Addr: 0x00000000004004e8 Offset: 0x0004e8 Link: 6 (.dynsym)
- 000: 0 (*local*) 2 (GLIBC_2.34) 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
- 004: 3 (GLIBC_2.2.5) 1 (*global*) 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
- 008: 3 (GLIBC_2.2.5)
5.4 hello的虚拟地址空间
由上述代码可知程序头的LOAD可加载程序段的地址为0x400000,使用edb加载hello,在Data Dump中观察hello加载到虚拟内存的情况。
图5-2 hello加载到虚拟内存情况
发现程序是从0x400000开始到0x401000被载入,到0x401ff0结束,最初为ELF表,和前ELF相同,包含此段的相关信息,根据节头可以得到各节所在的位置,如.text节的虚拟地址为0x4010f0,也可在上图找到对应信息。
5.5 链接的重定位过程分析
使用命令objdump -d -r hello > hello.asm生成反汇编文件hello.asm
图5-3 生成hello.asm
5.5.1 分析hello.asm与hello_o.asm的区别
与前面生成的hello_o.asm进行对比,发现以下不同之处。
- 函数数量增加
- Disassembly of section .plt:
- 0000000000401020 <.plt>:
- 401020: ff 35 e2 2f 00 00 push 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
- 401026: f2 ff 25 e3 2f 00 00 bnd jmp *0x2fe3(%rip) # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
- 40102d: 0f 1f 00 nopl (%rax)
- 401030: f3 0f 1e fa endbr64
- 401034: 68 00 00 00 00 push $0x0
- 401039: f2 e9 e1 ff ff ff bnd jmp 401020 <_init+0x20>
- 40103f: 90 nop
- 401040: f3 0f 1e fa endbr64
- 401044: 68 01 00 00 00 push $0x1
- 401049: f2 e9 d1 ff ff ff bnd jmp 401020 <_init+0x20>
- 40104f: 90 nop
- 401050: f3 0f 1e fa endbr64
- 401054: 68 02 00 00 00 push $0x2
- 401059: f2 e9 c1 ff ff ff bnd jmp 401020 <_init+0x20>
- 40105f: 90 nop
- 401060: f3 0f 1e fa endbr64
- 401064: 68 03 00 00 00 push $0x3
- 401069: f2 e9 b1 ff ff ff bnd jmp 401020 <_init+0x20>
- 40106f: 90 nop
- 401070: f3 0f 1e fa endbr64
- 401074: 68 04 00 00 00 push $0x4
- 401079: f2 e9 a1 ff ff ff bnd jmp 401020 <_init+0x20>
- 40107f: 90 nop
- 401080: f3 0f 1e fa endbr64
- 401084: 68 05 00 00 00 push $0x5
- 401089: f2 e9 91 ff ff ff bnd jmp 401020 <_init+0x20>
- 40108f: 90 nop
- Disassembly of section .plt.sec:
- 0000000000401090 <puts@plt>:
- 401090: f3 0f 1e fa endbr64
- 401094: f2 ff 25 7d 2f 00 00 bnd jmp *0x2f7d(%rip) # 404018 <puts@GLIBC_2.2.5>
- 40109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010a0 <printf@plt>:
- 4010a0: f3 0f 1e fa endbr64
- 4010a4: f2 ff 25 75 2f 00 00 bnd jmp *0x2f75(%rip) # 404020 <printf@GLIBC_2.2.5>
- 4010ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010b0 <getchar@plt>:
- 4010b0: f3 0f 1e fa endbr64
- 4010b4: f2 ff 25 6d 2f 00 00 bnd jmp *0x2f6d(%rip) # 404028 <getchar@GLIBC_2.2.5>
- 4010bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010c0 <atoi@plt>:
- 4010c0: f3 0f 1e fa endbr64
- 4010c4: f2 ff 25 65 2f 00 00 bnd jmp *0x2f65(%rip) # 404030 <atoi@GLIBC_2.2.5>
- 4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010d0 <exit@plt>:
- 4010d0: f3 0f 1e fa endbr64
- 4010d4: f2 ff 25 5d 2f 00 00 bnd jmp *0x2f5d(%rip) # 404038 <exit@GLIBC_2.2.5>
- 4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010e0 <sleep@plt>:
- 4010e0: f3 0f 1e fa endbr64
- 4010e4: f2 ff 25 55 2f 00 00 bnd jmp *0x2f55(%rip) # 404040 <sleep@GLIBC_2.2.5>
- 4010eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 函数调用call指令发生改变
链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器修改为所分配的虚拟地址。
- 401148: e8 43 ff ff ff call 401090 <puts@plt>
- 跳转指令发生改变
链接过程中,链接器解析了重定位条目,修改了跳转指令后的参数为分配的虚拟地址。
- 40115e: eb 56 jmp 4011b6 <main+0x91>
5.5.2 分析重定位过程
重定位分为两步,首先是重定位节和符号定义,将所有类型相同的节合并为同一类型的节,然后将运行时的内存地址赋给新节,至此每条指令和全局变量都有唯一的运行内存地址。接下来是重定位节中的符号引用,修改代码节和数据节中对每个符号的引用,使得其指向正确的运行时地址,这依赖于可重定位目标模块中重定位条目。
如下代码为hello.o反汇编中一个puts函数调用,发现为相对地址引用。
- 23: e8 00 00 00 00 call 28 <main+0x28>
- 24: R_X86_64_PLT32 puts-0x4
- 28: bf 01 00 00 00 mov $0x1,%edi
根据hello的反汇编可知addend为-4,ADDR(puts)为0x401090,refaddr为0x401125+0x24,其中0x401125为main的起始地址,计算得到refptr为ADDR(puts)+addend-refaddr为0x401090-0x4-0x401125-0x24,转换为补码为FFFFFF43,按照小端序为43 FF FF FF。
也可以根据公式:引用对象地址-下一条指令地址的差值补码,计算0x401090-(0x401125+0x28)差的补码,结果同上。
- 401148: e8 43 ff ff ff call 401090 <puts@plt>
以上代码为hello的反汇编代码中调用puts函数部分,发现机器码部分与刚才的推理一致。
5.6 hello的执行流程
通过edb调试来记录call命令进入的函数。
图5-4 edb调试图
开始执行:_start、_libe_start_main
执行main:_main、printf、_exit、_sleep、getchar
退出:exit
程序名 程序地址
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x4010a0
_sleep 0x4010e0
_getchar 0x4010b0
_exit 0x4010d0
5.7 Hello的动态链接分析
动态链接是指把程序按照模块分成多个独立部分,在程序运行时才链接在一起,形成完整程序,调用共享库函数时,会为该引用生成重定位记录,然后在程序加载时候动态链接器会解析它,其中延迟绑定是通过GOT和PLT实现的,可知GOT起始表位置为0x404000。
GOT表位置在调用dl_init之前0x404008后的16个字节均为0。但是在调用了dl_init之后会改变。
图5-5 调用dl_init后的字节
5.8 本章小结
本章阐述了链接的基本概念和作用,展示了使用命令链接生成hello可执行文件,观察了hello文件ELF格式下的内容,利用edb观察了hello文件的虚拟地址空间使用情况,最后以hello程序为例对重定位过程、执行过程和动态链接进行分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:一个执行中程序的实例,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
作用:进程为程序提供了一种假象,程序好像是独占地使用处理器和内存,处理器好像是无间断地一条又一条地执行我们程序中的指令。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一个交互型应用级程序,也被称为命令解析器,为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。
处理流程:首先从终端读入输入的命令,对输入的命令解析,如果该命令为内置命令,则立即执行命令,如果不是内置命令,则调用fork创建一个新的子进程,在该子进程的上下文中执行指定的程序。判断该程序为前台程序还是后台程序,如果为前者则等待程序执行结束,如果为后者则将其放到后台并返回。Shell可以接受对键盘输入的信号并对其处理。
6.3 Hello的fork进程创建过程
输入指令:./hello 2022110524 肖博予 18947959954 4
首先Shell判断该指令不是内置指令,于是父进程调用fork函数创建一个新的子进程,该子进程得到与父进程用户级虚拟地址空间相同的一份副本,包含代码段和数据段、堆、用户栈以及共享库。在父进程中,fork返回子进程的PID,而在子进程中fork返回0。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个程序。
int execve(const char *filename, const char *argv[], const char *envp[]);
其中filename为可执行目标文件,argv为参数列表,envp为环境变量。只有当出现错误,如找不到filename时,execve才会返回到调用程序,否则从不返回。
图6-1 新程序启动后的栈结构
6.5 Hello的进程执行
hello程序在运行时,
进程提供给应用程序的抽象有:
- 一个独立的逻辑控制流,好像我们的进程独占地使用处理器
- 一个私有的地址空间,好像我们的程序独占地使用CPU内存
操作系统提供的抽象有:
- 逻辑控制流,即程序计数器PC值的序列,一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流并发地运行
- 上下文切换,操作系统内核使用上下文切换这种异常控制流来实现多任务,内核为每一个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需状态
- 时间片,即一个进程执行它的控制流的一部分的每一时间段
- 用户模式和内核模式,一个运行在内核模式的进程可以执行指令集中的所有指令,而且可以访问系统中的任何内存位置。而一个运行在用户模式的进程不允许执行特权指令,也不能直接引用地址空间中内核区的代码和数据
- 上下文信息,上下文就是内核重新启动一个被抢占的进程所需状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈以及各种内核数据结构等对象的值构成。在hello程序执行过程中,在进程调用execve后,进程为hello程序分配新的虚拟地址空间,开始时程序运行在用户模式,调用printf输出Hello 2022110524 肖博予 18947959954,之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式,运行过程中,CPU不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用CPU来实现进程的调度
6.6 hello的异常与信号处理
6.6.1 异常种类
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
6.6.2 常见信号
ID | 名称 | 默认行为 | 相应事件 |
2 | SIGINT | 终止 | 来自键盘的中断 |
9 | SIGKILL | 终止 | 杀死程序 |
11 | SIGSEGV | 终止 | 无效的内存引用 |
14 | SIGALRM | 终止 | 来自alarm函数的定时器信号 |
17 | SIGCHLD | 忽略 | 一个子进程停止或终止 |
6.6.3 运行结果及相关命令
(1)正常运行状态
在程序正常运行时,打印10次提示信息,输入回车即可退出程序
图6-2 正常运行状态
- 运行时按下Ctrl + C
在程序运行时按下Ctrl + C,shell进程收到SIGINT信号,shell结束并回收hello进程
图6-3 运行时按下Ctrl + C
- 运行时按下Ctrl + Z
在程序运行时按下Ctrl + Z,shell进程收到SIGSTP信号,shell显示屏幕提示信息并挂起hello进程
图6-4 运行时按下Ctrl + Z
- ps和jobs命令
对hello进程的挂起可由ps和jobs命令查看,发现hello进程确实是被挂起,而非被回收,且其job代号分别为1和2
图6-5 运行ps和jobs命令
- pstree命令
输入pstree命令,可以将所有进程以树状图显示:
图6-6 运行pstree命令
- kill命令
输入kill命令,即可杀死指定进程
图6-7 运行kill命令
- fg命令
输入fg 1 命令将hello进程再次调到前台执行,可以发现shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句,程序仍然可以正常结束,并完成进程回收
图6-8 运行fg命令
- 随意按键
在程序运行过程乱按造成的输入均缓存到stdin,当getchar的时候读出一个‘\n’结尾的字符来作为一次输入,hello程序结束后,stdin中的其他字符串会被当作shell的命令行输入
图6-9 执行过程中随意按键
6.7本章小结
本章的主要内容是探讨计算机系统中的进程和shell,通过一个简单的hello程序,介绍了进程的概念和作用、shell的作用和处理流程,详细分析了hello程序的进程创建、启动和执行过程,最后对hello程序可能出现的异常情况,以及运行结果中的各种输入进行了解释和说明。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:也叫相对地址,要经过寻址计算才可得到存储器中物理地址,逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量。
线性地址:是逻辑地址到物理地址变换中间的一步,在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。
虚拟地址:程序访问存储器所使用的逻辑地址为虚拟地址,虚拟地址要经过地址翻译得到物理地址。
物理地址:在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理将程序分成若干个段进行存储,每个段都是一个逻辑实体,并通过段表进行管理。段表包括段号(段名)、段起点、装入位和段的长度等信息。程序被划分为多个块,如代码段、数据段和共享段等。
一个逻辑地址由两部分组成,包括段标识符和段内偏移量。段标识符由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,后3位包含一些硬件细节。索引号直接在段描述符表中找到一个具体的段描述符,段描述符具体地址描述了一个段,而多个段描述符组成了段描述符表。
全局描述符表(GDT)是系统中唯一的,它包含:
- 操作系统使用的代码段、数据段和堆栈段的描述符
(2)各任务、程序的LDT(局部描述符表)段
每个任务程序有一个独立的LDT,包括:
- 对应任务/程序私有的代码段、数据段和堆栈段的描述符
(2)对应任务/程序使用的门描述符,如任务门和调用门等
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组,每块称为一个虚拟页,利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个位地址组成,其中MMU可以利用页表来实现从虚拟地址到物理地址的翻译。
图7-1 页式管理示意图
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生虚拟地址VA,虚拟地址VA传送给MU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到PPN并与PPO(VPO)组合起来构成物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。
图7-2 地址转化示意图
图7-3 多级页表工作示意图
7.5 三级Cache支持下的物理内存访问
高速缓存存储区Cache的结构将地址位划分为t个标记位,s个组索引位,b个块偏移位,如果选中的组的有效位为1,而且标记位与地址中的标记位相匹配,我们就得到了一个缓存命中,否则就称为缓存不命中。如果缓存不命中,那么它需要从存储器层次结构的下一层中取出被请求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中,具体替换哪一行要取决于具体的替换策略。
图7-4 高速缓存存储器组织结构
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时候,内核会为新进程创建各种数据结构,并分配给其唯一的PID,为了给这个新进程创建虚拟内存,创建了当前进程的mm_struct、区域结构和页表的原样副本,当fork在新进程中返回时候,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任何一个在后来写操作时,写时复制就会创建新页面,为每个进程保持了私有地址空间的抽象。
图7-5 私有的写时复制对象示意图
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域。hello程序与共享对象1ibc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器。设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
图7-6 映射用户地址空间区域
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。处理程序执行如下步骤:
1)处理器生成一个虚拟地址,并将其传送给MMU
2)MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE
3)高速缓存/主存向MMU返回PTE
4) PTE的有效位为零, 因此 MMU 触发缺页异常
5) 缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘)
6) 缺页处理程序调入新的页面,并更新内存中的PTE
7) 缺页处理程序返回到原来进程,再次执行导致缺页的指令
图7-7缺页异常示意图
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有两种基本方法与策略。
1)带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
2)显示空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要介绍了存储器地址空间、段式管理、页式管理,介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:B0,B1 ,B2……Bm-1,所有的I/O设备(网络、磁盘、终端)都被模型化为文件,甚至内核也被映射为文件,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。设备的模型化:文件。设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,即描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件中的常量可以代替显式的描述符值。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发EOF条件,应用程序能检测到这个条件。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
(1)进程通过调用open函数打开一个存在的文件或者创建一个新文件。函数为:int open(char* filename,int flags,mode_t mode)。open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件;mode参数指定了新文件的访问权限位。
(2)进程通过调用close函数关闭一个打开的文件。函数为int close(fd)。fd是需要关闭的文件描述符,成功返回0,错误返回-1。关闭一个已关闭的描述符会出错。
(3)应用程序通过分别调用read和write函数来执行输入和输出。函数分别为ssize_t read(int fd,void *buf,size_t n)和ssize_t write(int fd,const void *buf,size_t n)。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则返回值表示的是实际传送的字节数量。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
- int printf(const char *fmt, ...)
- {
- int i;
- char buf[256];
- va_list arg = (va_list)((char*)(&fmt) + 4);
- i = vsprintf(buf, fmt, arg);
- write(buf, i);
- return i;
- }
vsprintf() 函数为int vsprintf(char *str, const char *format, va_list arg)。
其中str 是指向一个字符数组的指针,该数组存储了 C 字符串。format是字符串,包含了要被写入到字符串 str 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %[flags][width][.precision][length]specifier。arg 是一个表示可变参数列表的对象。这应被 <stdarg> 中定义的 va_start 宏初始化。如果成功,则返回写入的字符总数,否则返回一个负数。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
- int getchar(void)
- {
- static char buf[BUFSIZ];
- static char* bb = buf;
- static int n = 0;
- if(n == 0)
- {
- n = read(0, buf, BUFSIZ);
- bb = buf;
- }
- return(--n >= 0)?(unsigned char) *bb++ : EOF;
- }
getchar函数会从stdin输入流中读入一个字符。调用getchar时,会等待用户输入。输入回车后,输入的字符会存放在缓冲区中。第一次调用getchar时,需要从键盘输入,但如果输入了多个字符,之后的getchar会直接从缓冲区中读取字符。getchar的返回值是读取字符的ASCII码,若出错则返回-1。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
(第8章1分)
结论
hello所经历的过程:
首先由程序员将hello代码从键盘输入,依次要经过以下步骤:
1、预处理(cpp)。将hello.c进行预处理,将文件调用的所有外部库文件合并展开,生成一个经过修改的hello.i文件。
2、编译(ccl)。将hello.i文件翻译成为一个包含汇编语言的文件hello.s。
3、汇编(as)。将hello.s翻译成为一个可重定位目标文件hello.o。
4、链接(ld)。将hello.o文件和可重定位目标文件和动态链接库链接起来,生成一个可执行目标文件hello。
5、运行。在shel1中输入./hello 2022110524 肖博予 18947959954 4
6、创建进程。终端判断输入的指令不是shell内置指令,调用fork函数创建一个新的子进程。
7、加载程序。shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
8、执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
9、访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
10、信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
11、终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
感悟:
通过本次实验,我深切感受到计算机系统的精细和强大,每一个简单的任务都需要计算机的各种复杂的操作来完成,这背后体现出了严谨的逻辑和现代工艺的精巧。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 功能 |
hello.c | 源程序 |
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello_out.elf | 用readelf读取hello得到的ELF格式信息 |
hello_o.asm | 反汇编hello.o得到的反汇编文件 |
hello.asm | 反汇编hello可执行文件得到的反汇编文件 |
hello | 可执行文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
- Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.
- https://blog.csdn.net/rabbit_in_android/article/details/49976101
[3] printf 函数实现的深入剖析[转载]_如何查看printf的函数体-CSDN博客
[4] 内存地址转换与分段_计算机地址如何转化为内存-CSDN博客
[5] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[6] Ubuntu系统预处理、编译、汇编、链接指令_ubuntu脚本对数据预处理-CSDN博客
[7] https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)