计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术模块
学 号 2022110793
班 级 22WL022
学 生 马海斌
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文介绍了程序员第一课,即经典程序“hello world”从“出生”到“死亡”的过程,即p2p、020的过程,从程序到进程, 从运行到最后被回收的过程。在这个过程中通过查找资料和研究学习,了解了一个程序在计算机系统中经历的一些列处理过程,加深了对计算机系统工作流程和工作模式的理解。
关键词:hello world;计算机系统;程序;进程
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
Hello的P2P是指program to process,翻译过来是由程序到进程。从我们在ide或者文本编辑器中编写的C语言程序到进程的过程。hello.c通过cpp预处理生成ASCII码的中间文件hello.i,之后经过cc1翻译成汇编语言文件hello.s,接着as将hello.s转换为一个可重定位目标文件hello.o,最后运行ld,将hello.o和一些必要的文件组合起来,创建一个可执行目标文件。在shell中,通过系统的进程管理,成为一个进程。
Hello的020是指from zero-0 to zero-0,说的是内存数据的从无到有再到无,通过shell中的execute函数将hello载入内存为其分配空间。当程序结束后,进程又被回收,内核删除内存里关于hello的数据,完成to zero-0。
1.2 环境与工具
X64 CPU;2.80GHz;16G RAM;512G SSD
软件环境
Windows 11 64位;VMWare Workstation Pro 17;Ubuntu 22.04.2;LTS 64位
开发工具
Visual Studio 2022 64位;Visual Studio Code 64位;CodeBlocks 32位;gedit+gcc
1.3 中间结果
最终生成的可执行文件 | |
hello.c | 源程序 |
hello.elf | 由hello.o生成的elf文件 查看各节信息 |
hello_elf.elf | 由hello生成的elf文件 查看各节信息 |
hello.i | hello.c预编译得到的文件 |
hello.o | 汇编生成的可重定位目标文件 |
hello.s | 由hello.c编译生成的汇编语言代码 |
hello.objdump | hello.o的反汇编代码文件 |
hello_.objdump | hello的反汇编代码文件 |
表1-1:生成的中间结果文件的名字,文件的作用
1.4 本章小结
本章讲述了hello程序p2p,020的过程,从程序到进程的过程,还有一个程序的生命周期,还对我的环境与工具进行了介绍,最后,列举了在调试hello过程中的中间结果,和它们的作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程.典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——预处理记号用来支持语言特性。
作用:
1、将源文件中以”include”格式包含的文件复制到编译的源文件中。
2、用实际值替换用“#define”定义的字符串。
3、根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
图2.1 预处理过程
2.3 Hello的预处理结果解析
图2.2 预处理结果
打开生成的预处理文件,发现一共生成了3061行,但只有最后14行的部分是原来程序的部分。
图2.3 预处理结果的开头部分
文件的开始部分是一系列外部库.h文件路径。
图2.4 预处理结果的typedef部分
然后是一系列typedef函数,将头文件所用到的别名对应到标准数据类型中。
图2.5 预处理结果的内部函数
以及一些内部函数的声明等。
2.4 本章小结
这一部分介绍了hello预处理的过程,并分析了hello预处理的文件hello.i。
我们可以学习到通过宏指令可以使我们的代码过呢更加简洁,可读性更加好。预处理过程也使本来比较残缺的hello.c,变成更加完全的hello.i。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。
作用:分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。即将完整代码hello.i转换成汇编语言的hello.s。
3.2 在Ubuntu下编译的命令
图3.1 编译命令
3.3 Hello的编译结果解析
图3.2 汇编结果
3.3.1常量
- 字符型常量
图3.3 字符型常量
将printf打印的字符串常量保存到.LC0中。
- 其他常量
图3.4 其他常量
cmpl比较argv和4是否相等,如果不相等,就按照上述的字符串常量地址加载到寄存器中,然后打印。
3.3.2变量、运算
- 局部变量
i是局部变量,用来作为for语句中的循环量。
图3.5 局部变量
从%rbp可以看出i被保存在栈中。
- 运算
图3.6 运算方面
通过一系列addl指令,来完成对for循环中局部变量i的自增操作。
3.3.3数组、指针
图3.7 数组和指针
在main函数中有一个字符串数组argv,其中argc表示argv中参数的个数。
而atoi函数调用了argv数组:
图3.8 argv保存位置
-32(%rbp)是argv首地址,通过加24操作,移动到argv[3]的地址。
3.3.4控制转移
图3.9 控制转移指令
通过cmpl函数和jle函数比较i和9的大小并实现跳转,如果i>9就会跳出循环。
3.3.5函数调用
- main
传入参数为argc和argv,为系统调用,且参数中shell中传入,返回值为0.
- printf
图3.10 printf函数参数调用分析
通过设置寄存器%rdi和%rsi的值传入参数来调用。
- exit
图3.11 exit函数参数调用分析
通过设置寄存器%rdi和%rsi的值传入参数来调用。
- atoi
图3.12 atoi函数参数调用分析
将%eax的值设为argv[4],并赋值给%edi,作为传入参数来调用。
3.4 本章小结
本章介绍了从完整代码hello.i文件汇编成hello.s文件的过程,以及汇编语言下各部分变量、控制转移、函数调用和运算的等等的实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指把汇编语言书写的程序翻译成与之等价的机器语言程序的过程。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。
作用:汇编器(as)将.s汇编程序翻译成机器语言并将这些指令打包成可重定目标程序的格式存放在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。汇编过程从汇编程序得到一个可重定位目标文件,以便后续进行链接。
4.2 在Ubuntu下汇编的命令
图4.1 汇编指令
4.3 可重定位目标elf格式
图4.2 可重定位目标elf格式指令
图4.3 elf可重定位目标文件格式
图4.4 elf可重定位目标文件(部分)
ELF头以16字节的magic序列开始,这个序列描述了生成该文件的系统的字的大小和字节吮吸,elf头文件剩下的部分包含帮助连接器语法分析和解释目标文件的信息,其中包括elf头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
ELF头中还包括程序的入口点,也就是程序运行时要执行的第一条指令的地址为0x0。
图4.5 elf可重定位目标文件
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF可重定位目标文件格式由ELF头,节头部表,和夹在两者之间的一些节组成。
具体包括:
ELF 头、程序头表、.text 节、.rodata节、.bss节、.symtab节、.rel.txt节、.rel.data节、.debug节、节头表
ELF 头可用readelf -l main查看,它包含了字大小、字节序,文件类型(.o,exec,.so)、机器类型、节头表的位置、条目大小、数量等
·段头表/程序头表:页面大小,虚拟地址内存段(节),段大小
·.text 节:包含已编译程序的机器代码
·.rodata 节:只读数据 : printf的格式串、跳转表, ...
·.data 节 :已初始化全局和静态变量
·.bss 节 :未初始化/初始化为0的全局和静态变量。仅有节头,但节本身不占用磁盘空间
·.symtab 节(符号表):函数和全局/静态变量名,节名称和位置
·.rel.text 节(可重定位代码):.text 节的可重定位信息,在可执行文件中需要修改的指令地址,需修改的指令
·.rel.data 节(可重定位数据):data 节的可重定位信息,在合并后的可执行文件中需要修改的指针,数据的地址
·.debug 节(调试符号表):为符号调试的信息 (gcc -g)
·节头表:每个节的在文件中的偏移量、大小等
当汇编器生成hello.o后,它并不知道数据和代码最终将存放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,他就会生成一个重定位条目,告诉连接器在将目标文件合并成可执行文件时,如何修改这个引用。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
图4.6 hello.o
上图中左侧为机器语言反汇编的结果,右侧为汇编程序。
可以看到机器语言程序是由机器指令组成的,反汇编中每一行的1到5个字节不等的16进制数表示就是一条机器指令,对应汇编语言中的一行。机器指令是可以变长的,常用的,操作数少的指令字节数少;不常用的,操作数多的指令字节数多。
分支跳转和函数调用不一样,在hello.s中,分支跳转的目标位置是通过.L1,.L2这样的助记符来实现的,而hello.o中,跳转的目标位置是指令的位置。
函数调用在hello.s中,call后的目标函数是它的函数名,而在hello.o中,call的是目标函数的相对偏移量的值。
4.5 本章小结
本章分析了汇编的过程,并分析了ELF头、节头部表、符号表以及重定位节。表交了hello .s和hello.o反汇编之后的代码的不同。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
作用:使得分离编译成为可能。我们可以独立的修改和编译我们需要修改的小的模块,而不必将全部的程序重新编译一次,简化了维护和管理。
5.2 在Ubuntu下链接的命令
图5.1 链接命令
图5.2 生成文件
5.3 可执行目标文件hello的格式
图5.3 可执行目标文件hello(部分)
图5.4 可执行目标文件hello(部分)
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
Hello(可执行文件)的ELF与hello.o的ELF都包含ELF 头,程序头表,.text 节,.rodata节,.bss节,.symtab节,.debug节,节头表。
但是可执行文件里少了.rel.txt节和.rel.data节,因为在链接的过程中他们的任务已经完成了故生成的可执行文件里不再包含.rel.txt节和.rel.data节。但是新增了和动态链接库相关的重定位信息.rela.dyn 。
同时,新增了.init节
各段的起始地址,大小等信息可以在节头部表里查看。
5.4 hello的虚拟地址空间
图5.5.1 edb分析hello(1)
使用edb打开hello,可以看到hello的开始地址位0x4010000
图5.5.2 edb分析hello(2)
例如.text文件起始于0x4010f0,然后我们查看这个内存单元
图5.5.3 edb分析hello(3)
例如.data文件起始于0x404048,查看这个内存单元。
5.5 链接的重定位过程分析
图5.6 hello.o(部分)
图5.7 hello.o(部分)
可以看到,hello.o的反汇编结果中存在着大量类似于
的代码,可以看到对应机器代码部分本来应该是printf地址的地方全是0,与此同时下面还给出了重定位的类型:R_X86_64_PC32和addend:-0x4。
而在hello的反汇编结果中,这些全是0的空位已经被填好了,即
且是运行时的绝对地址,无需进一步修改。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出入口点,也就是 _start 函数的地址。这个函数是在系统目标文件 ctrl.o 中定义的。_start 函数调用系统启动函数 __libc_start_main,该函数定义在 libc.so 中。它初始化执行环境,调用用户层的 main 函数,处理 main 函数的返回值,并且在需要的时候把控制返回给内核。
图5.8 edb分析hello的执行流程
ld-2.31.so!_dl_catch_exception@plt <0x00007f9fddc6d010>
ld-2.31.so!malloc@plt <0x00007f9fddc6d020>
ld-2.31.so!_dl_signal_exception@plt <0x00007f9fddc6d030>
ld-2.31.so!calloc@plt <0x00007f9fddc6d040>
ld-2.31.so!realloc@plt <0x00007f9fddc6d050>
ld-2.31.so!_dl_signal_error@plt <0x00007f9fddc6d060>
ld-2.31.so!_dl_catch_error@plt <0x00007f9fddc6d070>
ld-2.31.so!_dl_rtld_di_serinfo <0x00007f9fddc77090>
ld-2.31.so!_dl_debug_state <0x00007f9fddc7e1d0>
ld-2.31.so!_dl_mcount <0x00007f9fddc7fe00>
ld-2.31.so!_dl_get_tls_static_info <0x00007f9fddc80680>
ld-2.31.so!_dl_allocate_tls_init <0x00007f9fddc80770>
ld-2.31.so!_dl_allocate_tls <0x00007f9fddc809a0>
ld-2.31.so!_dl_deallocate_tls <0x00007f9fddc80a10>
ld-2.31.so!_dl_make_stack_executable <0x00007f9fddc81130>
ld-2.31.so!_dl_find_dso_for_object <0x00007f9fddc81480>
ld-2.31.so!_dl_exception_create <0x00007f9fddc84ca0>
ld-2.31.so!_dl_exception_create_format <0x00007f9fddc84da0>
ld-2.31.so!_dl_exception_free <0x00007f9fddc85250>
ld-2.31.so!__tunable_get_val <0x00007f9fddc865d0>
ld-2.31.so!__tls_get_addr <0x00007f9fddc86da0>
ld-2.31.so!__get_cpu_features <0x00007f9fddc86df0>
ld-2.31.so!malloc <0x00007f9fddc89490>
ld-2.31.so!calloc <0x00007f9fddc895b0>
ld-2.31.so!free <0x00007f9fddc895f0>
ld-2.31.so!realloc <0x00007f9fddc897e0>
ld-2.31.so!_dl_signal_exception <0x00007f9fddc89a70>
ld-2.31.so!_dl_signal_error <0x00007f9fddc89ac0>
ld-2.31.so!_dl_catch_exception <0x00007f9fddc89c40>
ld-2.31.so!_dl_catch_error <0x00007f9fddc89d30>
hello!_init <0x0000000000401000>
hello!puts@plt <0x0000000000401030>
hello!printf@plt <0x0000000000401040>
hello!getchar@plt <0x0000000000401050>
hello!atoi@plt <0x0000000000401060>
hello!exit@plt <0x0000000000401070>
hello!sleep@plt <0x0000000000401080>
hello!_start <0x00000000004010f0>
hello!_dl_relocate_static_pie <0x0000000000401120>
hello!main <0x0000000000401125>
hello!__libc_csu_init <0x00000000004011c0>
hello!__libc_csu_fini <0x0000000000401230>
hello!_fini <0x0000000000401238>
5.7 Hello的动态链接分析
在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件 hello。此时共享库中的代码和数据没有被合并到 hello 中。只有在加载 hello 时,动态链接器才对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。
比如查看 _GLOBAL_OFFSET_TABLE 的内容:
在运行前:
运行dl_init后
图5.9 hello的动态链接分析
5.8 本章小结
本章详细介绍了hello的连接过程,比对链接后的hello与hello.o的不同,最后使用gdb工具逐行查看了hello的运行过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程提供给应用程序两个关键抽象(假象):一个独立的逻辑控制流、一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个交互型的应用级程序,它代表用户运行其他程序。Shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。求值过程调用parseline函数,切分命令行来构造将会传给execve的argv数组。解析完命令行之后,eval函数调用builtin_command函数检查是否是内置命令:如果命令行第一个参数是一个shell内置命令名,那么shell会立刻执行这个命令;否则shell认为这个参数是一个可执行目标文件的名字,它会在一个新的子进程的上下文中加载并运行这个文件。如果最后一个参数是&,shell不会等待这个命令完成,否则表示在前台执行,shell会等待它完成。
6.3 Hello的fork进程创建过程
当shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
fork函数只被调用一次,却会返回两次。一次是在调用进程中,一次是在新创建的子进程中。在父进程中,fork返回子进程的pid,在子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
创建过程:
(1)给新进程分配一个标识符
(2)在内核中分配一个PCB,将其挂在PCB表上
(3)复制它的父进程的环境(PCB中大部分的内容)
(4)为其分配资源(程序、数据、栈等)
(5)复制父进程地址空间里的内容(代码共享,数据写时拷贝)
(6)将进程置成就绪状态,并将其放入就绪队列,等待CPU调度
6.4 Hello的execve过程
execve() 函数加载并运行可执行目标文件,且带参数列表 argv 和环境变量列表 envp,execve() 函数调用一次从不返回。它的执行过程如下:
1.删除已存在的用户区域
2.映射私有区:为 hello 的代码、数据、.bss 和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的
3.映射共享区:比如 hello 程序与共享库 libc.so 链接
4.设置 PC:exceve() 做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点
5.execve() 在调用成功的情况下不会返回,只有当出现错误时,例如找不到需要执行的程序时,execve() 才会返回到调用程序
6.5 Hello的进程执行
6.5.1.逻辑控制流
操作系统将一个 CPU 物理控制流,分成多个逻辑控制流,每个进程独占一个逻辑控制流。当一个逻辑控制流执行的时候,其他的逻辑控制流可能会临时暂停执行。一般来说,每个逻辑控制流都是独立的。当两个逻辑控制流在时间上发生重叠,我们说是并行的。处理器在多个进程中来回切换称为多任务,每个时间当处理器执行一段控制流称为时间片。因此多任务也指时间分片。
6.5.2 用户模式与内核模式
为了限制一个应用可以执行的指令以及它可以访问的地址空间范围,处理器用一个控制寄存器中的一个模式位来描述进程当前的特权。
用户模式:用户模式中的进程不允许执行特权指令,比如停止处理器、改变模式位,或者发起一个 I/O 操作。也不允许用户模式的进程直接引用地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。
进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
6.5.3 上下文切换
操作系统内核为每个进程维护一个上下文。所谓上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
6.5.4 hello的执行
从 Shell 中运行 hello 时,它运行在用户模式,运行过程中,内核不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用执行,实现进程的调度。如果在运行过程中收到信号等,那么就会进入内核模式,运行信号处理程序,之后再返回用户模式。
6.6 hello的异常与信号处理
hello执行过程中出现的异常种类可能会有:中断、陷阱、故障、终止。
中断:中断是来自处理器外部的I/O设备的信号的结果。如在按下ctrl-z后,会触发一个中断异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则处理程序返回到内核中的abort例程并将其终止。
终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。
图6.1 程序正常运行结果
正常运行过程中,输出10次“Hello 2022110793 马海斌 18004420535”,再输入一个字符并回车后退出。(由于我的手机号除以5的余数为0,打印过快难以触发中断异常,故选取时间间隔为3)
图6.2 输入乱码或回车后程序运行结果
乱按或者回车对程序运行无影响。
图6.3 程序运行过程中输入ctrl-c后的结果
在按下ctrl-c后,触发一个中断异常,内核向shell发送SIGINT信号,在SIGINT的处理程序中,shell向前台进程组发送信号,使得前台进程组的所有成员终止。
图6.4 程序运行过程中输入ctrl-z后的结果
按下ctrl-z后,触发一个中断异常,内核向shell发送SIGTSTP信号,在SIGTSTP信号处理程序中,shell向前台进程组发送信号,使得前台进程组的所有成员停止。
输入jobs,看到hello进程已经停止。
图6.5 输入ps命令的结果
输入ps可以看到hello进程仍然存在。
图6.6 输入pstree命令后的结果(部分)
图6.6 输入pstree命令后的结果(部分)
图6.6 输入pstree命令后的结果(部分)
输入pstree可以看到所有进程以树的方式显示。
图6.7 输入fg,kill命令后的运行结果
输入fg,hello进程回到前台,正常执行直至退出。
输入kill -9 hello的进程号后,向hello进程发送了一个SIGKILL信号,使得这个进程终止。
6.7本章小结
本章讲解了 hello 如何运行在操作系统的上下文中,以及它如何受到信号的控制。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
我们的 hello 进程是与其它进程共享 CPU 和主存资源的,为了更加有效地管理内存并且少出错,现代操作系统提供了一种对主存的抽象概念,叫做虚拟内存。虚拟内存时硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。首先确定一些概念:
- 1.逻辑地址:格式为“段地址:偏移地址”,是 CPU 生成的地址,在内部和编程使用,并不唯一。
- 2.线性地址:逻逻辑地址到物理地址变换之间的中间层,逻辑地址经过段机制后转化为线性地址。
- 3.虚拟地址:保护模式下,hello 运行在虚拟地址空间中,它访问存储器所用的逻辑地址。
- 4.物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU 通过地址总线的寻址,找到真实的物理内存对应地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台下的实模式中,逻辑地址为:CS:EA,CS 是段寄存器,将 CS 里的值左移四位,再加上 EA 就是线性地址。而保护模式下,要用段描述符作为下标,到 GDT(全局描述符表)/LAT(局部描述符表)中查表获得段地址,段地址+偏移地址就是线性地址。段描述符是一个 16 位字长的字段,如图:
图7.1描述符图示
TI 位指示选择 GDT 还是 LDT,前 13 位作为索引来确定段描述符在描述符表中的位置。从段描述符和偏移地址得到线性地址的过程如图:
图7.2获取线性地址的流程
7.3 Hello的线性地址到物理地址的变换-页式管理
VM 系统将虚拟内存分割为成为虚拟页的大小固定的快,物理内存也被分割为物理页,成为页帧。虚拟页面就可以作为缓存的工具,被分为三个部分:
·未分配的:VM 系统还未分配的页
·已缓存的:当前已缓存在物理内存中的已分配页
·未缓存的:未缓存在物理内存的已分配页
如图:
图7.3 页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
页表是 PTE(页表条目)的数组,它将虚拟页映射到物理页,每个 PTE 都有一个有效位和一个 n 位地址字段,有效位表明该虚拟页是否被缓存在 DRAM 中,地址字段表明 DRAM 中相应物理页的起始位置,它分为两个部分:VPO(虚拟页面偏移)和 VPN(虚拟页号),如图:
图7.4 从虚拟地址到物理地址
7.4.1 TLB加速地址翻译
为了优化 CPU 产生一个虚拟地址后,MMU 查阅 PTE的过程,在 MMU 中设置一个关于 PTE 的小缓存,称为 TLB(翻译后备缓冲器)。像普通的缓存一样,TLB 的索引和标记是从 PTE 中的 VPN 提取出来的,如图:
图7.5 TLB提取
7.4.2 四级页表翻译
下面举Core i7的例子。
图7.6 四级页表翻译
每次 CPU 产生一个虚拟地址后,通过它的 VPN 部分看 TLB 中是否缓存,如果命中,直接得到 PPN,将虚拟地址中的 VPO 作为物理页偏移,这样就能得到物理地址;如果 TLB 未命中,则经过四级页表的查找得到最终的PTE,从而得到 PPN,进而得到物理地址。
7.5 三级Cache支持下的物理内存访问
得到物理地址后,将物理地址分为 CT(标记位)、CI(组索引) 和 CO(块偏移)。根据 CI 查找 L1 缓存中的组,依次与组中每一行的数据比较,有效位有效且标记位一致则命中。如果命中,直接返回想要的数据。如果不命中,就依次去 L2、L3 缓存判断是否命中,命中时将数据传给 CPU 同时更新各级缓存。
7.6 hello进程fork时的内存映射
在 Shell 输入命令行后,内核调用fork创建子进程,为 hello 程序的运行创建上下文,并分配一个与父进程不同的PID。通过 fork 创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当 fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
execve() 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:
- 1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的 .text 和 .data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
- 3.映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
- 4.设置程序计数器(PC),execv() 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
DRAM 缓存不命中称为缺页,以下图为例假设CPU引用了(虚拟页)VP 3中的一个字VP 3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE 3,从有效位推断VP 3未被缓存,并且触发一个缺页异常。缺页异常会调用内核中的缺页异常处理程序,缺页处理程序就执行下面步骤:
1)查看虚拟地址是否合法,若不合法,那么缺页处理程序就触发一个段错误,从而终止这个进程;
2)查看试图进行的内存访问是否合法,如果试图访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程;
3)选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送虚拟地址到MMU,MMU就能正常翻译它了,而不会再产生缺页中断了。
在此例中就是牺牲页就是存放在(物理页)PP 3中的VP 4。如果VP 4已经被修改了,那么内核就会将它复制回磁盘。无论哪种结果,内核都会修改VP 4的页表条目,反映出VP 4不再缓存在主存中这一事实。接下来,内核从磁盘复制 VP 3 到内存中的 PP 3,更新 PTE 3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP 3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。
图7.7 异常发生情况及其处理
7.9动态存储分配管理
7.9.1 堆
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,如图:
图7.8 堆
-
-
- 隐式空闲链表管理
-
想要设计好的数据结构维护空闲块需要考虑以下方面:
·空闲块组织:利用隐式空闲链表记录空闲块
·放置策略:如何选择合适的空闲块分配?
·首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块
·下一次适配:从上一次查询结束的地方开始搜索选择第一个合适的空闲块
·最佳适配:搜索能放下请求大小的最小空闲块
·分割:在将一个新分配的块放置到某个空闲块后,剩余的部分要进行处理
·合并:释放某个块后,要让它与相邻的空闲块合并
隐式空闲链表中的每个块由头部、有效载荷,以及可能的一些额外的填充组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。头部后面就是应用程序调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。
为什么既设置头部又设置尾部呢?这是为了能够以常数时间来进行块的合并。无论是与下一块还是与上一块合并,都可以通过他们的头部或尾部得知块大小,从而定位整个块,避免了从头遍历链表。空闲块怎么组织呢?如图:
图7.9 空闲块的处理方法
- 为了消除合并空闲块时边界的考虑,将序言块和结尾块的分配位均设置为已分配。为了保证双字对齐,在序言块的前面还设置了 4 个字节作为填充。
7.9.3 显式空闲链表管理
显式空闲链表的已分配块的块结构和隐式链表的相同,由一个字的头部、有效载荷、可能的一些额外的填充以及一个脚部组成。
而在每个空闲块中,增加了一个前驱指针和后继指针。通过这些指针,可以将空闲块组织成一个双向链表。
空闲链表中块的排序策略包括后进先出顺序、按照地址顺序维护、按照块的大小顺序维护等。显式空闲链表降低了放置已分配块的时间,但空闲块必须足够大,以包含所需要的指针、头部和脚部,这导致了更大的最小块大小,潜在提高内部碎片程度。
图7.10 空闲块的处理方法
简单分离存储:从不合并与分离,每个块的大小就是大小类中最大元素的大小。例如大小类为 {17~32},则需要分配块的大小在这个区间时均在此对应链表进行分配,并且都是分配大小为 32 的块。这样做,显然分配和释放都是常数级 的,但是空间利用率较低。
分离适配:每个大小类的空闲链表包含大小不同的块,分配完一个块后,将这个块进行分割,并根据剩下的块的大小将其插入到适当大小类的空闲链表中。这个做法平衡了搜索时间与空间利用率,C 标准库提供的 GNU malloc 包就是采用的这种方法。
7.10本章小结
Hello在运行时会涉及很多有关于存储系统的事情。首先是在shell里运行时,fork和execve会对进程的虚拟地址空间进行复制,删除,和映射,当hello真正开始运行时,由于execve只是建立了到磁盘上对应区域的映射,在开始运行时必定会触发一次缺页故障,缺页处理程序将调入hello所需的页面。Hello在运行时如果访问了某处的地址,必须经过从虚拟地址到物理地址的转换,另外hello只能访问自己有访问权限的地址空间,否则就会触发段错误。
(第7章 2分)
结论
Hello的一生:
程序员为他编写最初的代码 — hello.c
预处理器完善代码 — hello.i
编译器将它转化为汇编语言 — hello.s
汇编器为它的诞生做出最后的准备 — hello.o
链接器让它成为一个完整的程序 — hello
Shell为它创建子进程,使他成为系统的一份子
加载器映射虚拟内存
cpu的逻辑流时他交叉于用户与内核之间
Hello加载到内存,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache协同工作,完成对地址的翻译和数据的读写
信号与异常约束它的行为,使它不偏离正道
最后shell回收子进程,内核删除与它相关的一切
Hello world作为我最初接触计算机方面知识的时候第一个成功实现出来的程序,对我有非凡的意义,在经历了一学期对计算机系统这门课的学习后,我发现表面上如此简单的程序,其背后的实现过程竟是如此的庞大且复杂,但同时它的一生也是完整的、精妙的,让我领略到计算机系统的魅力。
感悟:
计算机系统是由硬件和软件组成的,他们共同工作来运行程序.虽然系统的实现方法一直在变化,但其内在的概念们没有改变.所有的系统都有相似的硬件和软件,也执行着相似的功能,了解这些功能对于程序员来说不仅是了解程序是怎样运行的,代码会经过几次处理才能实现想要的效果,更重要的是能根据这种实现方式解决代码可能的安全漏洞,或是对代码进行优化处理,加速程序的运行,又或是对出现的报错有更深层次的理解,能迅速给出解决方案。
通过对这门课程的学习,对于这些技巧有了一定的了解,但是只有在实践中才能更熟练的使用这些技巧。
附件
hello | 最终生成的可执行文件 |
hello.c | 源程序 |
hello.elf | 由hello.o生成的elf文件 查看各节信息 |
hello.i | hello.c预编译得到的文件 |
hello.o | 汇编生成的可重定位目标文件 |
hello.s | 由hello.c编译生成的汇编语言代码 |
dump.txt | hello.o的反汇编代码文件 |
dump1.txt | hello的反汇编代码文件 |
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
(参考文献0分,缺失 -1分)