计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术
学 号 2021111629
班 级 21W0312
学 生 吕俊增
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
本文主要阐述了一个短短几行代码组成的hello.c文件是怎么在linux系统中一步步地经过预处理、编译、汇编、链接等过程,在CPU、RAM、Cache、OS等的处理下,形成了形如hello.i、hello.s、hello.o、hello.elf、hello等产物,运行后在shell中结束“生命”的过程
关键词:预处理;编译;汇编;链接;进程;异常与信号处理;虚拟内存;存储管理;I/O管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
编写后的hello.c文件,先经过预处理器预处理生成hello.i;再经过编译器编译生成汇编代码文件hello.s;再经过汇编器翻译成一个重定位目标文件hello.o;最后使用链接器将多个可重定位目标文件组合起来,形成一个可执行目标文件hello。在shell中运行hello,用fork为其创建进程并用execve执行程序,cpu为hello分配时间片,以流水线的形式执行hello。OS和MMU会为hello程序设置从虚拟内存空间到物理内存的映射,硬件系统也会通过TLB,4级页表,3级cache等方式加快程序运行速度。当内核发现异常时会抛出信号并将运行权限转给信号处理程序,待处理完成后再转回hello。hello运行结束后,由shell回收进程,删除有关的数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件:Lenovo拯救者
软件:Windows 10 64位;Vmware 15.5;Ubuntu 20.04.4
使用工具:Codeblocks;edb等
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
helloO.elf:hello.o的ELF格式
hello.elf:hello的ELF格式
helloO.o.txt:hello.o反汇编代码
hello.txt:hello的反汇编代码
1.4 本章小结
描述了hello.c从编写到运行到最后被回收的过程,说明了本次大作业所使用到的软硬件和运行环境,以及使用的工具。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
(以下格式自行编排,编辑时删除)
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。
根据以’#’开头的命令(如include,define)修改原始的C程序。预处理会得到另一个c程序,一般以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
在hello.i中,仍然有源c程序代码,但还会新增加很多新内容,预处理将hello.c中的#开头的命令扩展为具体的代码实现,并对#define命令的符号进行了替换。最后将hello.c不含#的命令原样复制到hello.i的结尾。
2.4 本章小结
介绍了预处理的概念与作用,并将hello.c在Ubuntu上生成了一个预处理文件hello.i,之后阅读该文件得到其作用
第3章 编译
3.1 编译的概念与作用
概念:读取hello.i对其进行语法分析,将C语言转换成等价的汇编语言指定,得到汇编代码hello.s文件
作用:将用户编写的高级语言程序转换成汇编语言程序,方便后续的汇编和链接操作
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
- 字符串:存在两个只读字符串描述格式化输出格式:
.LC0:
.string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"
.LC1:
.string "Hello %s %s\n"
2.局部变量i
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
栈指针开辟32个字节大小的空间,将局部变量i保存在-20(%rbp)
3.3.2伪指令
.file "hello.c"
.text
.section .rodata
.align 8
其中.file 声明源文件,.text指示代码段,.section指示rodata段。
3.3.3 赋值:
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
利用MOV类进行赋值
3.3.4计算
只存在一个算数操作i++,由于i是int,用addl指令实现该算数操作
3.3.5 数组/指针/结构操作:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
movq -32(%rbp), %rax
addq $24, %rax
唯一一个数组*argv[],该数组的起始元素被存储在栈中rbq-32的位置,通过数组第一个元素加上偏移量来访问数组中的任意元素
存在指针数组*argv[],只需访问指针所保存的地址来访问对应的元素即可
3.3.6 控制转移:
cmpl $4, -20(%rbp)
je .L2
If:使用cmp将argc与4比较,如果不等于4,则直接跳转到L2执行。
.L2:
movl $0, -4(%rbp)
jmp .L3
addl $1, -4(%rbp)
.L3:
cmpl $8, -4(%rbp)
jle .L4
For:先使用movl将局部变量i赋值为0,之后每次操作完成使用算术指令add给i加1,之后使用cmpl将i与8比较,小于8的话跳转到L4。
3.3.7 函数操作:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
通过call指令调用main、printf等函数,调用时会将下一条指令压入栈中,然后将参数按从指定寄存器到栈的顺序存放,当某个程序调用这些寄存器,被调用寄存器会先保存这些值然后再进行调用,在调用结束后恢复被调用前的值。
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
将printf转换成了puts,把L0段的值传入%rdi,然后call跳转到puts。这里的exit是把数1传入到%edi中,然后call跳转到exit。
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
addl $1, -4(%rbp)
这里的printf有三个参数,第一个在.LC1中的格式化字符串%eax中,后面的两个是%rdi,%rsi,之后跳转到printf。sleep有一参数传到%edi中,之后call跳转到 sleep中。
3.4 本章小结
写了编译的概念和作用,对hello.c编译并解读其各功能的实现
第4章 汇编
4.1 汇编的概念与作用
概念:汇编阶段就是把编译阶段生成的”.s”文件转成可重定位目标“.o”文件的过程。
作用:把汇编语言变成可以识别的机器语言,便于后面链接阶段识别。
4.2 在Ubuntu下汇编的命令
指令:gcc -c hello.c -o hello.o
4.3 可重定位目标elf格式
指令readelf -a hello.o > hello.elf得到hello.elf文件。
4.3.1ELF头:ELF头是在ELF文件开头以一个16字节的序列为起始的部分,这一序列描述了系统的字的大小和字节顺序。后面的部分则是用来帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件类型、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。(节头部表描述了不同节的位置和大小,目标文件的每个节都在其中有一个固定大小的条目)。
4.3.2节头表:节头表是包含各个节的信息的表格,包括节的名称、类型、地址、偏移量、大小、flags、链接、信息和对齐方式。
4.3.3重定位节:ELF格式文件中有一部分重定位条目的节,其中储存了重定位条目的信息,包括偏移量,符号,重定位类型和addend值等,用来提供给后续链接步骤中的链接器进行重定位操作。.rela.txt中存放全局变量和被调用函数的重定位条目,.rela.data存放初始化数据的重定位条目,.rela.eh_frame节则存放.eh_frame节的重定位条目。
4.3.4符号表:.symtab是符号表,包括了程序中定义和引用的函数与变量的信息。想要在ELF格式文件中获得符号表,不需要在编译时使用-g选项,每个ELF文件在.symtab中都有一个符号表,但与编译器中的符号表不同,ELF中的符号表不包括局部变量的信息。程序员可以用STRIP命令去掉.symtab。
4.4 Hello.o的结果解析
4.4.1转移控制:
argv在反汇编得到的结果中存储在-0x14(%rbp)中,而hello.s中他会跳转到L2,在反汇编中,他经过重定位,会跳转到7d5这个确定的位置。
将i与8作比较,如果小于,跳转到34的位置。
4.4.2 函数操作:
在汇编语言中,函数调用是直接使用函数名来调用的,而在机器语言中则改为使用偏移量的表示方法,比如该程序中调用printf就是使用相对于main的偏移作为地址,并在命令后附上一个printf的重定位条目,用于后续链接时对printf的调用进行重定位。
4.5 本章小结
本章介绍了生成hello的过程中汇编步骤的概念和作用,并分析了ELF格式的结构和hello.o的反汇编结果文件来理解汇编步骤中各操作的实现,熟悉了阅读反汇编语言
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:通过命令将多个模块拼接为一个独立可执行的程序的过程就叫做链接。执行的时机有:(1)编译时:源代码被翻译成机器码。静态库,共享库(动态库),可重定位目标文件;(2)加载时:程序被加载器加载到内存并执行时。静态库,共享库(动态库),可重定位目标文件;(3)运行时:由应用程序来执行链接。共享库(动态库)/共享目标文件。
作用:链接可将多个可重定位目标文件合并生成一个可执行目标文件。
5.2 在Ubuntu下链接的命令
Ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 -L/usr/lib/x86_64-linux-gnu/crt1.o -L/usr/lib/x86_64-linux-gnu/crti.o -L/usr/lib/x86_64-linux-gnu/libc.so -L/usr/lib/x86_64-linux-gnu/crtn.o -lc hello.o
5.3 可执行目标文件hello的格式
readelf -a hello > hello.elf得到hello.elf
- 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: 0x400450
Start of program headers: 64 (bytes into file)
Start of section headers: 5296 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 19
Section header string table index: 18
和可重定位文件的ELF头一样描述了文件的总体格式,但它还多了程序的入口点,即当程序运行时要执行的第一条指令的地址。
- 节头:
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 00000000004001c8 000001c8
000000000000001c 0000000000000000 A 0 0 1
[ 2] .hash HASH 00000000004001e8 000001e8
0000000000000030 0000000000000004 A 4 0 8
[ 3] .gnu.hash GNU_HASH 0000000000400218 00000218
000000000000001c 0000000000000000 A 4 0 8
[ 4] .dynsym DYNSYM 0000000000400238 00000238
00000000000000a8 0000000000000018 A 5 1 8
[ 5] .dynstr STRTAB 00000000004002e0 000002e0
000000000000003b 0000000000000000 A 0 0 1
[ 6] .gnu.version VERSYM 000000000040031c 0000031c
000000000000000e 0000000000000002 A 4 0 2
[ 7] .gnu.version_r VERNEED 0000000000400330 00000330
0000000000000020 0000000000000000 A 5 1 8
[ 8] .rela.plt RELA 0000000000400350 00000350
0000000000000090 0000000000000018 AI 4 14 8
[ 9] .plt PROGBITS 00000000004003e0 000003e0
0000000000000070 0000000000000010 AX 0 0 16
[10] .text PROGBITS 0000000000400450 00000450
000000000000008e 0000000000000000 AX 0 0 1
[11] .rodata PROGBITS 00000000004004e0 000004e0
0000000000000033 0000000000000000 A 0 0 8
[12] .eh_frame PROGBITS 0000000000400518 00000518
0000000000000060 0000000000000000 A 0 0 8
[13] .dynamic DYNAMIC 0000000000600eb0 00000eb0
0000000000000150 0000000000000010 WA 5 0 8
[14] .got.plt PROGBITS 0000000000601000 00001000
0000000000000048 0000000000000008 WA 0 0 8
[15] .comment PROGBITS 0000000000000000 00001048
0000000000000029 0000000000000001 MS 0 0 1
[16] .symtab SYMTAB 0000000000000000 00001078
00000000000002e8 0000000000000018 17 20 8
[17] .strtab STRTAB 0000000000000000 00001360
00000000000000b7 0000000000000000 0 0 1
[18] .shstrtab STRTAB 0000000000000000 00001417
0000000000000096 0000000000000000 0 0 1
包含了ELF文件中各个节的信息,包括名称、大小、偏移量等
- 段头:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x0000000000000188 0x0000000000000188 R 0x8
INTERP 0x00000000000001c8 0x00000000004001c8 0x00000000004001c8
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000578 0x0000000000000578 R E 0x200000
LOAD 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0
0x0000000000000198 0x0000000000000198 RW 0x200000
DYNAMIC 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0
0x0000000000000150 0x0000000000000150 RW 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0
0x0000000000000150 0x0000000000000150 R 0x1
hello的ELF文件多出了一个Program Headers,即程序头部表
4)节到段的对应
Hello的ELF中加入了从节到段的对应,表明一个段中都包括了哪些节。
5)重定位节
ELF文件中仍有重定位节来储存重定位条目的信息。
6)符号表
可执行文件中的符号表部分存储着所有符号的信息
5.4 hello的虚拟地址空间
Init函数和fini函数对应两个特殊的节.init和.fini,它们会在main函数运行前和运行后执行,从节头表可知两者地址分别是0x401000和0x401238,这也恰好对应了虚拟地址空间的开头和结尾。而在ELF头中可知程序切入点main的地址为0x4010f0,可推测该地址为init和main的分界线
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello指令
在hello得到的反汇编文件中比起hello.o的多了很多节,同时他的数据有了明确的地址,而不是像hello.o的一样,从0x0开始。多的是许多形如<function>的节,是经链接后,在可执行文件中生成的外部函数。当运行到需要这些函数时,程序会根据call指令后经过重定位了的地址跳转到对应的.text节或.data节,这样机器就能直接知道外部函数或全局变量的具体位置,不必再进行其他的操作或修改。
5.6 hello的执行流程
_start:0x4010f0
__libc_start_main:0x7ffff7de3fc0
Main:0x401125
_printf:0x4010a0
_sleep:0x4010e0
_getchar:0x4010b0
Exit:0x4010d0
_fini:0x401238
5.7 Hello的动态链接分析
发现.got节中的内容发生改变,是因为在.got节中存放的是变量的全局偏移量
我们打开ELF文件,找到节头表,找到.got节的首地址,是0x403ff0,在执行链接之前,如第一张图所示,链接后如第二张图所示。
5.8 本章小结
主要对hello.c的链接和ELF文件格式做了详细介绍,使用edb和obdjump对连接过程中虚拟空间的使用和动态链接过程进行分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程提供了逻辑控制流和私有地址空间两个抽象,两者可以让程序员以为自己的程序在独占地使用处理器和存储器。进程的引入简化了程序员的编程和语言处理系统的处理,即简化了编程、编译、链接、共享和加载的整个过程。
概念:进程是一个执行中的程序的实例,指程序的一次运行过程。进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因此进程具有动态含义。
作用:进程提供了逻辑控制流和私有地址空间两个抽象,两者可以让程序员以为自己的程序在独占地使用处理器和存储器。进程的引入简化了程序员的编程和语言处理系统的处理,即简化了编程、编译、链接、共享和加载的整个过程。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是运行在终端中的文本互动程序,shell将我们的操作或命令解释为计算机可识别的二进制命令,传递给内核,以便调用计算机硬件执行相关的操作;计算机执行完命令后,再通过shell解释为成自然语言。
处理流程:
1)用户输入指令,经过字符串处理函数后被分割和处理为相应的命令和参数,空指令会被跳过
2)检查是否有shell的内置命令,有则直接执行。
3)读取命令与参数并创建子进程,在子进程中加载并运行程序。
4)父进程等待子进程退出后回收子进程,然后父进程退出。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程,新创建的子进程几乎与父进程相同:子进程得到与父进程虚拟地址空间相同的一份副本,子进程获得与父进程任何打开文件描述符相同的副本,而最大的区别则是子进程有不同于父进程的PID。fork函数总共被调用一次,返回两次。
6.4 Hello的execve过程
在fork产生的子进程中,shell会通过execve函数加载hello程序到此进程中。该函数声明为:int execve(char *filename, char *argv[], char *envp[]),hello的数据将会覆盖当前进程的代码、数据、栈并对其初始化,进行内存映射,而保留相同的PID,继承已打开的文件描述符和信号上下文。最后将PC指向hello程序第一个执行的函数。
6.5 Hello的进程执行
每个进程都可以看做是一个逻辑控制流,进程每个执行它自己的一段时间就是一个时间片,即使是两个并发的逻辑控制流,在一个被另一个打断后也可以重新回到断点继续运行。内核是内存中管理进程的操作系统代码块,进程的物理实体和支持进程运行的环境被称为进程的上下文,分为用户级上下文和系统级上下文,在上下文切换时,系统将原进程的寄存器上下文移动到系统级上下文的现场信息位置。通过内核模式作为中介,系统将用户模式下的控制流从一个进程转移到另一个进程
在hello中,程序执行了sleep系统调用,引发了陷阱异常。此时会从用户模式进入内核模式,使程序休眠一段时间,将控制转给其他进程。当sleep结束后,发送信号给内核,进入内核状态处理异常,此时hello程序得以重新回到用户模式。
6.6 hello的异常与信号处理
(1)在程序运行时按回车,会多打印几处空行,其余正常。
(2)crtl+Z
在hello进程运行的过程中输入ctrl+Z程序会暂停并挂起前台作业hello,通过ps指令可以发现此时hello进程并没有被回收,在接收到SIGCON信号后会继续运行
(
- ctrl c
输入ctrl+C后程序停止,使用ps查看后发现当前进程没有hello。Ctrl c会使内核发送SIGINT信号到前台进程组的每个进程,包括hello进程,调用信号处理函数后,返回的默认结果是终止hello进程,同时shell程序会回收hello进程。
- 乱输
程序正常进行,只是多出了乱输入的内容
6.7本章小结
主要写了进程的概念与作用以及壳Shell-bash的作用与处理流程,展示hello的进程执行过程及异常与处理
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
1)逻辑地址:hello在编译后在汇编语言中出现的地址,通过一个标识符来确定其中一条指令位置之后通过偏移量来确定同标识符下其他指令的位置。
2)线性地址:线性地址是逻辑地址通过段机制转化得来的地址,算是虚拟地址的一种,同时也是逻辑地址到物理地址变换之间的中间层。通过段基址+段内偏移地址来找到对应的代码。
3)虚拟地址:虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成的数组,通过页表的帮助以页为单位映射到物理内存上去。
4)物理地址:物理意义上的内存器件中的存储单元的地址,处理器通过物理地址来查找内存条上的数据位置
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址=段选择符+偏移量
每个段选择符大小为16位,段描述符为8字节。每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中。而要想找到某个段的描述符必须通过段选择符即可找到。
通过段选择符我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
如果CPU启用了MMU,CPU核发出的地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址,如下图所示
Linux采用了分页(paging)的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页(page)来管理内存。在Linux中,通常每页大小为4KB。
7.4 TLB与四级页表支持下的VA到PA的变换
1)TLB
TLB即快表,名为翻译后备缓冲器,是页表的缓存,储存有一些经常使用的页表条目,实现虚拟页面向物理页面的映射,对于页面数很少的页表可以完全包含在TLB中。,MMU在接收到VA后会先根据TLBI和TLBT的内容在TLB中寻找PTE,如果未命中的话再去页表中寻找,由于TLB较小且有较高相连度,因此要比直接寻找页表要快,从而加快VA到PA的转换。
2)四级页表
当虚拟地址太多时,一个页表的PTE数量已经无法满足虚拟地址的长度了,此时就要将虚拟地址的VPN分割为数个部分,前一个VPN的PTE对应着作为下一个VPN的寻找范围的页表,这样就能通过相连的页表来表示更长的虚拟地址,这就是多级页表的目的。对于四级页表,就是将VPN分割为VPN1到VPN4,VPN1在第一级页表中找到PTE1,该PTE1指向数个二级页表中的一个,后续也同理寻找,直到VPN4对应的PTE4指向了一个物理内存,则VA到PA的变换结束。
由上述两种结构实现变换:步骤1:CPU产生一个虚拟地址。步骤2:MMU从TLB中获取相应的PTE。步骤3:MMU把这个虚拟地址翻译成物理地址,并把它发送到高速缓存/主存。步骤4:高速缓存/主存将请求的数据字返回给CPU.当TLB没有命中时,MMU必须从Ll高速缓存中检索相应的PTE.新检索的PTE被存储在TLB中,并可能覆盖已经存在的条目。
7.5 三级Cache支持下的物理内存访问
在MMU获得PA后,将根据PA前往高速缓存中寻找数据,根据缓存组的数量将PA分为CT、CS、CO,根据CS找到正确的组,并比较每个cacheline,看标记位是否有效,CT是否相等。如果在L1中没有命中,则前往L2中进行相同操作,L3同理。如果命中,则直接返回所需的数据,命中后将数据传给CPU并更新各级缓存的cacheline。
7.6 hello进程fork时的内存映射
Fork函数为新进程创建虚拟内存,他会创建当前进程的的mm_struct, vm_area_struct和页表的原样副本。将两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW),在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
execve 函数在当前进程中加载并运行包含在可执行目标文件hello.out中的程序,用hello
.out程序有效地替代了当前程序。加载并运行hello.out需要以下几个步骤:
1)删除已存在的用户区域。
(2)映射私有区域。
(3)映射共享区域。
(4)设置程序计数器。
7.8 缺页故障与缺页中断处理
(1)段错误: 访问一个不存在的页面,此时会中断程序执行。
(2)正常缺页:会选择牺牲页,换入新的页面并进行页表更新,完成缺页处理。
(3)保护异常: 例如,违反许可,写一个只读的页面(Linux 会显示 Segmentation fault)
7.9动态存储分配管理
malloc函数:
malloc的全称是memory allocation,中文叫动态内存分配。作用是向系统申请分配指定size个字节的内存空间,函数原型为:
extern void *malloc(unsigned int num_bytes);
Malloc从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。关于堆的知识呢可以查询数据结构方面的知识。在使用malloc()分配内存空间后,一定要记得释放内存空间,否则就会出现内存泄漏。
7.10本章小结
本章介绍了系统对于hello的存储管理,介绍了逻辑地址、线性地址、物理地址、虚拟地址,从逻辑地址到物理地址的变换过程,以及从虚拟地址寻找物理地址的过程中经由MMU、TLB、Cache等许多部件的处理与应对过程。此外还解释了fork与execve的内存映射、缺页故障以及动态储存分配管理的过程。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,经由这种处理Linux内核给出的一个简单、低级的应用接口,能够以统一且一致的方式执行 I/O操作,这就是unix io接口。
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2.Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
系统级I/O函数:
(1)打开文件
函数原型:int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。flags参数指明了进程打算如何访问这个文件,mode参数则指定了新文件的访问权限位。
(2)关闭文件
函数原型:int close(int fd);
关闭描述符为fd的文件,关闭一个已关闭的描述符会出错。
(3)读和写文件
函数原型:
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的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
首先是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;
}
(va_list)((char*)(&fmt) + 4)代表的是printf参数中的第一个参数,printf的参数数量不确定。
vsprintf可以接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,返回的是要打印出来的字符串的长度。
之后我们来看write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
其中ebx存有字符串的第一个地址,ecx存有字符串长度,之后来看INT_VECTOR_SYS_CALL:
init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);
简单来说就是要通过系统来调用sys_call这个函数。而sys_call实现为:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
sys_call只实现一个功能:显示格式化了的字符串。它将通过总线将字符串中的字节从寄存器复制到显卡的显存。显存存储ASCII字符码,字符显示驱动子程序将控制从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
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调用了read函数读取键盘上的输入按键ascii码,,getchar会从缓冲区提取字符而不是直接从键盘输入。getchar调用read函数,将缓冲区读入到buf中,并将长度送给n,再重新令bb指针指向buf。最后返回buf中的第一个字符(如果长度n < 0,则报EOF错误)。getchar直到接受到回车键才返回一次,且只返回第一个字符作为getchar函数的值,如果有循环或足够多的getchar语句,就会依次读出缓冲区内的所有字符直到’\n’。
8.5本章小结
本章介绍了Linux的IO设备管理方法和Unix IO接口及其函数,并且详细论述了printf函数和getchar函数的执行流程,包括系统调用和IO管理。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1.编写:程序员使用c语言写出hello.c程序源代码
2.预处理:修改hello.c中的所有#命令,生成hello.i文件
3.编译:将hello.i文件翻译为汇编语言实现的hello.s文件
4.汇编:将hello.s文件翻译为机器码的可重定位目标文件hello.o
5.链接:调用链接器由hello.o生成可执行目标文件hello
6.运行:在shell中输入./hello <学号> <姓名> <间隔时长>
7.创建进程:shell调用fork创建子进程
8.加载程序:shell调用execve将hello程序加载到进程中运行
9.内存空间:系统为hello分配存储空间,完成虚拟内存和内存映射,运行期间hello程序通过MMU翻译虚拟地址来访问并使用对应的物理内存空间
10.异常与信号:运行过程中发生异常或收到信号时,中断控制流并转移到对应处理程序上,完成后控制流回到原位置。
11回收进程:运行完毕后,父进程回收子进程。
经过这次大作业,我感悟到了短短的几行代码,内涵着计算机的知识,感觉自身解决相关问题的能力有了显著的提高,hello的一步一步让我震撼于计算机的博大精深,让我有一种学无止境的感慨。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
helloO.elf:hello.o的ELF格式
hello.elf:hello的ELF格式
helloO.o.txt:hello.o反汇编代码
hello.txt:hello的反汇编代码
(附件0分,缺失 -1分)
参考文献
Linux连接文件的两种方式 TAN BO(CSDN)
逻辑地址,线性地址和物理地址转换(CSDN)
计组复习(四):cache,虚拟内存,页表与TLB(CSDN)
动态内存的管理(CSDN)
为完成本次大作业你翻阅的书籍与网站等
(参考文献0分,缺失 -1分)