计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机与电子通信
学 号 120L021612
班 级 2036013
学 生 王鹤蓬
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年5月
本文按照简单c语言程序hello.c生命周期的顺序对计算机系统进行了简单分析,并介绍了几个主要的计算机系统组成成分:预处理,编译,汇编,链接,进程管理,存储管理,I/O管理以及他们的功能
关键词:计算机系统;编译;汇编;链接;进程;I/O管理
目 录
第1章 概述
1.1 Hello简介
1.最初通过编辑器编写c语言文件,得到源程序hello.c
2.预处理器预处理源文件,加入头文件内容生成hello.i
3.编译器处理预处理后文件,生成汇编语言文件hello.s
4.汇编器将汇编语言文件生成为可重定位目标文件hello.o
5.将可重定位文件链接起来,生成可执行文件hello
6.在shell中利用fork为程序创建进程,调用execve分配内存,内核为程序分配时间片,I/O管理处理程序所需的输入和输出,最终进程结束后被回收,内核和内存回归到程序执行前的状态
1.2 环境与工具
硬件环境:X64 CPU;2GHz;4GRAM;256Disk
软件环境:Windows10;Unbuntu20.04 LTS 64位
开发工具:codeblocks;gdb
1.3 中间结果
hello.c:源程序
hello.i:预处理后文件
hello.s:汇编语言文件
hello.o:可重定位目标文件
hello:可执行文件
hello.elf:hello.o的elf格式
hello1.elf:hello的elf格式
hello1.txt:hello.o的反汇编
hello2.txt:hello的反汇编
1.4 本章小结
本章介绍了hello程序在计算机系统中的基本生命周期,同时也介绍了本次实验的开发环境等信息
第2章 预处理
2.1 预处理的概念与作用
预处理指预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。预处理可以读取c开头的命令并生成另一个程序,通常以.i结尾。
2.2在Ubuntu下预处理的命令
gcc hello.c -E -o hello.i
图2.2 预处理过程
2.3 Hello的预处理结果解析
预处理的结果如上图,可以看到.i文件共三千多行,预处理器将原.c文件开头中使用的<stdio.h>等头文件的内容都加入了.i文件中,将原代码段放进了.i文件的末尾。
2.4 本章小结
本章介绍了预处理的概念和相关作用,以及在unbuntu下的预处理实践。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是指译器(ccl)将文本文件hello.i翻译为文本文件hello.s的过程,编译过程将高级语言编写的程序转化为汇编语言表示的程序,同时还可以附带语法检查等功能。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 汇编程序开头指令
.file:声明源文件
.text:生命代码节
.section.rotation:声明只读代码段
.globl:声明全局变量
.string:声明字符串
.type:声明类型
3.3.2 数据
局部变量:原程序定义了局部变量i,在汇编程序中应该是放进了栈里,对应地址是RBP-4,如下图
传递参数:原程序传入main函数两个参数,一个是argc,与i一样被放入了堆栈中,另一个是数组argv,起始位置被放入堆栈中,如下图
3.3.3 赋值
用movq为寄存器赋值
3.3.4 函数
原程序唯一一个函数是main函数,用globl声明
3.3.5 算术操作
源程序中的算术操作是for循环中的i++,在汇编语言程序中是addl $1, -4(%rbp)
3.3.6 关系操作
源程序中有两个关系运算,一个是argc!=3,在汇编程序中如下
另一个关系运算是i<8,在汇编程序中如下
3.3.7 控制转移
源程序中的if比较对应着上一环节中argc关系运算对应的汇编语言,根据cmpl的结果设置条件码并跳转
3.3.8 函数操作
调用函数是通过让rip设置为函数Q代码的起始地址,并将原指令下一条指令的地址储存到栈里,然后在返回时,把rip设置为P中调用Q后面那条指令的地址。
共有三次函数调用,第一次调用puts函数,参数存在rdi中;
第二次调用printf函数参数1存在rdi中,参数2、3分别存%rsi和rdx中。
第三次调用sleep函数,以sleepsecs为参数,参数存在rdi中
3.4 本章小结
本章简单的分析了编译器处理文件的过程,以及针对c语言文件中调用,赋值,控制转移等操作编译器的处理方式。
第4章 汇编
4.1 汇编的概念与作用
汇编指汇编器将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中的过程。汇编过程的作用是将汇编语言转换为机器可以识别的机器指令。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf
- ELF头:
ELF头的开头是Magic,描述了生成该文件的系统的字的大小和字节顺序。接下来ELF头还包含了文件类型、机器类型等信息。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
2.节头:
节头记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐
3.重定位节
重定位节保存的是.text节中需要被修正的信息
4.符号表
符号表存放在程序中定义和引用的函数和全局变量的信息
4.4 Hello.o的结果解析
用objdump -d -r hello.o显示反汇编如下
反汇编文件主体与hello.s相同,不同点主要如下
- 反汇编文件由机器指令和汇编指令混合而成
- 反汇编文件缺少.s文件开头.开头的指令
- 分支转移:
Hello.s:
反汇编:
可以看到hello.s中的跳转用的是段,而反汇编中跳转直接使用了地址,推测是在汇编过程中汇编文件中的段不再存在,因此反汇编以后也不使用段名称。
2.函数调用:
Hello.s:
反汇编:
与分支转移类似,反汇编后的文件在调用函数的时候并不使用函数名称,而是下一条指令的地址。这是因为hello.c 中调用的函数需要通过链接器才能确定函数的执行地址,在汇编之后,对于这些不确定地址的函数,只能将其call指令后的地址设置为下一条指令的地址。
4.5 本章小结
本章分析了汇编的过程以及作用,同时利用反汇编指令分析了分析了汇编语言与机器语言的对应关系。
第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.3 可执行目标文件hello的格式
用readelf将hello输出为一个elf格式文件
- ELF头
Hello的ELF头与hello.o的ELF头类似,但是图中第八行的文件类型有所不同,hello的文件类型是EXEC可执行文件,第十九行的section headers数量也有所不同
2.节头
节头对hello中所有的节信息进行了声明,其中包括大小 Size 以及在程序中的偏移量,hello的节头除了节的数量与hello.o中有所不同以外并没有明显区别
3.重定位节
重定位节与hello.o中的有所不同
4.符号表
符号表存放在程序中定义和引用的函数和全局变量的信息
5.4 hello的虚拟地址空间
使用edb加载hello,如下
可以从data dump里发现hello的虚拟地址空间开始于0x400000,结束于0x40ff0
由5.3中elf文件的节头表可以找到各节所在的位置,如.interp节,
在edb中寻找该地址004002e0,可以找到.interp节的内容
5.5 链接的重定位过程分析
1.hello中跳转的指令均跟有确定的地址,如下
而hello.o跳转指令是当前指令的下一条指令,说明库函数已经链接进了hello中
2.hello中多了很多节,如
说明在连接过程中程序各个节变得完整
3.hello的重定位过程:(此处参考csdn)
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
5.6 hello的执行流程
(1)_start
(2)_libc_start_main
(3)main
(4)_printf
(5)_sleep
(6)_getchar
(7)_exit
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。延迟绑定是通过GOT和PLT实现的
由图可知.got.plt的起始地址在00404000,在edb中查找00404000地址的内容,如下,
运行程序到调用init之后,此时4040000地址的内容如下
可以看到.got.plt中的内容已经发生了改变,而其中含有的地址7f62ddc61190和7f62ddc4aae0均是运行时需要用到的函数或共享库的地址,可以看出plt+got链接器实现了函数的一个动态过程链接,这样一来,程序就已经包含了正确的绝对运行时地址。
5.8 本章小结
本章通过对hello可执行程序的分析,分析链接的基本过程,文件的重定位,动态链接以及可重定位目标文件ELF格式的各个节等与链接有关的内容。
第6章 hello进程管理
6.1 进程的概念与作用
进程的典型定义是一个针对执行中的应用程序的程序的实例。系统中的每个应用程序都可以运行在某个应用进程的可执行上下文中。
进程可以产生如下假象(也就是作用):
(1) 我们的程序好像是系统中当前运行的唯一程序一样,独占了处理器和内存空间。
(2) 处理器在无间断的执行我们的程序。
6.2 简述壳Shell-bash的作用与处理流程
1.作用
Shell是一个交互型应用级程序,为使用者提供操作界面,可以接收用户命令然后根据命令调用相应的应用程序。
- 处理流程
(1)读入命令。
(2)通过特定的函数读取命令生成参数表(数组)。
(3)检查第一个命令行参数是否是一个内置的shell命令,如果是则立即执行。
(4)如果不是内部命令,调用fork( )创建进程执行指定程序。
(5)在执行程序的时候还可以对用户的键盘操作进行反应,发出特定信号
6.3 Hello的fork进程创建过程
在终端中输入./hello 学号 姓名 秒数以后,终端会读取指令并判断hello是否是内置指令,由于hello不是内置指令,因此shell会fork创建一个子进程,并在子进程中准备运行hello程序。
6.4 Hello的execve过程
创建子进程后,子进程会调用execve函数运行hello程序,流程如下:
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
- 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
- 设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
hello进程的执行是依赖于进程所提供的抽象的基础上,而进程的抽象主要包含了以下几点
- 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 一些对象的值构成,包括通用目的寄存器,浮点寄存器等。内核在经历一个内存模式以后开始代表另一个进程执行指令。
- 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片
- 用户模式和内核模式:当没有中断等特殊事件时,程序运行在用户模式,此时不允许程序调用特权函数,也不允许程序访问内核区的数据,而当异常发生时,程序将转入内核模式,可以执行任何指令,访问任何内存。
抽象除了主要讨论的这四点以外还包括了私有地址空间,逻辑控制流,并发流等。
现在在抽象的层面上来看hello的进程执行,调用execve后hello开始执行,此时程序已经被分配了虚拟地址空间,运行在用户模式下。
当用户输入的时候,hello调用了getchar函数。实际执行的是系统调用read,在进行read调用之后程序请求来自键盘缓冲区的输入,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时内核执行上下文切换,在完成键盘缓冲区到内存的数据传输之前,内核将执行其他程序。数据传输完成以后引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。
在程序输出“hello 120L021612 王鹤蓬”之后,调用了sleep函数,此时发生上下文转换,hello进程进入内核模式,hello程序陷入休眠的同时内核开始代表其他进程执行指令,当sleep计数器结束以后引发一个中断信号,内核从其他进程进行上下文切换回hello进程。
6.6 hello的异常与信号处理
1.异常的种类
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:sleep函数执行期间产生陷阱类异常
故障:在执行hello程序的时候,可能会发生的潜在可恢复的错误。
终止:执行hello时发生的不可恢复的错误
2.信号的种类
信号的种类较多,常见信号比如SIGINT(来自键盘的中断),SIGKILL,SIGCHLD(子进程终止)等。
- hello的异常与信号处理
Hello的正常执行如下
Hello对回车输入的反应:
可以看到键盘输入回车并不影响程序执行
Hello对输入ctrl+Z的反应:
输入以后hello作业被挂起,此时输入ps命令查看当前进程,如下
可以看到hello进程没有终止,而是放进了后台。调用jobs可以发现hello的jid是1,然后输入fg 1将hello移到前台,
可以看到hello继续正常执行
Hello对ctrl+C的反应:
Ctrl+C会发送一个SIGINT到前台作业,默认行为是终止作业,此时输入ps查看进程,
可以看到hello进程已经被终止了。
6.7本章小结
在本章中,分析了进程的定义与作用,同时介绍了 Shell 作用、调用 fork 创建新进程、调用 execve函数 执行 hello,以及hello 的执行和异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:hello程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定hello中操作数(如argv)或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量。
- 线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
- 虚拟地址:同线性地址
- 物理地址:真实的物理内存对应地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(参考csdn)
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
7.3 Hello的线性地址到物理地址的变换-页式管理
使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的住哪用硬件,利用主存中的查询表来动态翻译虚拟地址。
虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小时相同的。
任意时刻虚拟页都被分为三个不相交的子集:
未分配的:VM系统还未分配的页
缓存的:当前已经缓存在物理内存的已分配页
未缓存的:当前未缓存在物理内存的已分配页
每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。如图7.3.1所示,页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在次胖的起始地址。
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
7.10本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
结论
1.hello.c:编写c程序
2.hello.i:hello.c经过预处理,补充内容变为hello.i。
3.hello.s:hello.i经过编译阶段变为汇编语言文件hello.s。
4.hello.o:hello.s经过汇编阶段变为机器语言文件hello.o。
5.hello:hello.o与可重定位目标文件和动态链接库链接成为可执行文件hello。
6.运行:在终端运行hello程序,并输入我的信息(120L021612 王鹤蓬 2)
7.创建子进程:shell调用fork()函数创建一个进程。
8.加载:shell 在子进程中调用execve,加载hello程序,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main 函数。
9.上下文切换:hello等待输入加载入内存时发生上下文切换,内核执行其他进程
10.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
11.终止:hello执行完成时,内核回收子进程,删除为这个进程创建的所有数据结构,恢复到执行hello之前的状态。
附件
hello.c:源程序
hello.i:预处理后文件
hello.s:汇编语言文件
hello.o:可重定位目标文件
hello:可执行文件
hello.elf:hello.o的elf格式
hello1.elf:hello的elf格式
hello1.txt:hello.o的反汇编
hello2.txt:hello的反汇编
参考文献
- Computer Systems - A Programmer’s Perspective ,Randal E.Bryant
- https://so.csdn.net/so/search?spm=1001.2101.3001.4498&q=%E6%B1%87%E7%BC%96&t=&u=,CSDN-汇编
- https://www.cnblogs.com/diaohaiwei/p/5094959.html