哈尔滨工业大学计算机系统实验4 HIT_CSAPP LAB4
哈工大计算机系统实验4
摘 要
本文按照《深入理解计算机系统》(第三版)一书,从程序的诞生到程序的运行与结束,深入分析了程序在每一步的细节,以及如何实现、每一部分的作用。全文以hello.c文件在Linux系统下,运行前后的预处理、编译、汇编、链接、进程管理、内存管理等的实现和观察,从可视化角度,使程序运行的每一步骤都清晰可见。在本过程中产生的中间文件都可以进行查看,保证了对于文件组成的细致观察,而进程与内存的观察也在linux系统指令的帮助下完成。使本文可以作为一份基本的对于程序运行的介绍。
关键词:程序运行; Linux系统;进程管理 ;内存管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
**
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
第1章 概述
1.1 Hello简介
首先,用C语言写出hello.c源程序文本文件,然后进行预处理,形成hello.i文本文件,再编译形成hello.s汇编语言文本文件,再进行汇编形成hello.o可重定位目标文件。最后进行链接,形成可执行目标程序ELF二进制文件。
执行阶段,操作系统fork一个子进程,用execve加载进程,完成P2P。进程结束后,擦欧总系统进行进程回收,实现020。
1.2 环境与工具
硬件环境:Intel® Core™ i7-10750H CPU @ 2.60GHz 2.59 GHz,16GB机带RAM。
软件环境:windows 10 64位;VM Ware;Ubantu 20
开发工具:gcc;gdb;objdump;
1.3 中间结果
hello.i:预处理得到的文本文件。
hello.s:hello.i编译后得到的汇编语言文本文件。
hello.o:hello.s汇编之后得到的可重定位目标文件。
hello.out:链接之后得到的ELF可执行目标程序二进制文件。
1.4 本章小结
生成可执行文件的过程P2P:编写高级语言源文件,编译,汇编,链接。最后就可以执行可执行文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理指的是在编译之前进行的处理。C语言的预处理主要有三个方面的内容:宏定义、文件包含、条件编译。预处理命令以符号“#”开头。
预处理工作也叫做宏展开,将宏名替换为文本。宏定义只需将符号常量替换成后面对应的文本即可。文件包含可以在一个文件中包含另一个文件的内容,被包含的文件称为头文件。头文件的内容可以有函数原型、宏定义、结构体定义。条件编译是在条件满足时才编译某些语句。
使用条件编译可以使目标程序变小,运行时间变短。同时有利于代码的模块化。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
图2-1 预处理指令
图2-2 hello.i文件
2.3 Hello的预处理结果解析
hello.i文件中有3060行,仍然可以看出大概的C语言语法。有着很多的全局变量和结构体定义,还有着一部分的固定的函数。
2.4 本章小结
hello.c文件经过gcc hello.c -E -o hello.i指令之后,预处理成了hello.i文本文件,在文本文件之中还能看到大概的c语言语法。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是指将一种程序语言(源语言)转换为另一种程序语言(目标语言)的过程。编译器是用于执行编译过程的软件工具。编译的作用是将高级语言编写的程序转化为机器语言,以便计算机能够理解和执行。
具有提高执行效率、实现跨平台支持、错误检查和源代码保护等作用。
3.2 在Ubuntu下编译的命令
命令:gcc hello.i -S -o hello.s
图3-1 编译指令
图3-2 hello.s文件内容
3.3 Hello的编译结果解析
图3-3 源程序
3.3.1 数据:变量(全局/局部/静态)、函数
程序中有一个全局函数main,局部变量i。
图3-4 头文件部分
3.3.2主函数参数
主函数中有两个参数,分别是int argc 和 char *argv[],分别存在栈寄存器中
图3-5 主函数参数
3.3.3 条件判断语句
cmpl语句进行条件判断,不等于4就继续执行,如果等于4就跳转至.L2处,顺序执行时调用了puts输出图3-3中的字符串,然后使用参数1调用exit结束程序。
图3-6 条件判断
3.3.4循环和主函数结尾
.L4是for循环部分,局部变量i存在栈寄存器%rbp指向的-4处,将i置零,然后用jmp进入.L3。在.L3中,cmpl进行条件判断,如果i小于9就进入.L4的函数中,说明循环9次。
在.L4中调用了printf函数和sleep函数,在调用printf函数时,访问了argv[1]和argv[2],而argv是指针数组,所以会进行二次寻找,先将argv[1]中的内容放入%edi中,然后调用printf函数进行打印。然后调用sleep,最后对i加一。
图3-7 for循环
3.4 本章小结
汇编代码和c语言已经有了较大的不同,而在汇编语言之中,if条件判断,for循环的执行都靠着跳转指令,所以读懂汇编代码十分重要,能够更加清楚电脑对于代码的实现方式。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
将.s文件(汇编语言源文件)转换为.o文件(目标文件)的过程称为汇编过程。在这个过程中,汇编器将汇编源代码转换为机器语言指令,并生成对应的目标文件,其中包含可执行程序或库的二进制表示形式。汇编过程的概念是将人类可读的汇编语言代码转换为计算机可执行的机器语言指令。汇编器根据汇编源代码中的指令、操作数和符号等信息,将其转换为二进制表示形式,并生成目标文件。目标文件包含了汇编代码中的指令、数据和符号信息,以供后续的链接过程使用。
作用:1.生成目标文件;2.为链接过程做准备;3.优化和重定位。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
图4-1 汇编指令
4.3 可重定位目标elf格式
使用命令readelf -h hello.o可以看到elf头中包含的内容。包含魔数、类别等。
图4-2 elf头
4.3.1头表
使用readelf -S hello.o查看节头表信息,和各节信息。
图4-3 节头表
4.3.2符号表
使用readelf -s hello.o查看符号表
图4-4 符号表
4.4 Hello.o的结果解析
命令操作:
图4-5 反汇编结果
1.机器语言的构成:机器语言是一种由二进制位组成的指令集,直接由计算机硬件执行的编程语言。它使用特定的二进制编码表示各种指令和操作数,对应于底层硬件的操作和寄存器。由操作码、操作数、寄存器构成。
2.映射关系:机器语言和汇编语言大致相同,但是在汇编语言中,操作数可以直接使用符号、标签或者变量名来表示不需要直接指定二进制编码。
3. 在汇编语言中,分支转移、函数调用等控制流操作通常使用标签或相对地址表示,而不是直接给出具体的机器语言地址。
4.5 本章小结
将hello.s汇编成hello.o文件之后,汇编代码转化成了机器语言指令,并生成了对应的目标文件,其中包含可执行程序或库的二进制表示形式。汇编代码于机器语言的反汇编代码之间的主要差别在于操作数。在汇编语言中,操作数可以直接使用符号、标签或者变量名来表示不需要直接指定二进制编码。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将多个可重定位目标文件合并为一个可执行程序或库文件的过程。链接器是负责执行链接操作的工具。主要分为两个步骤:1.符号解析;2.重定位。
链接解析符号引用和定义之间的关联,进行重定位,消除重复的代码和数据,并将库文件与目标文件进行合并。链接器的工作确保了程序的正确运行,并产生最终可执行文件或库文件,供系统执行和使用。
5.2 在Ubuntu下链接的命令
命令:gcc hello.o -o hello.out
图5-1 链接命令
5.3 可执行目标文件hello的格式
命令:readelf -h hello.out、readelf -S hello.out、readelf -s hello.out
图5-2 elf头
图5-3 节头表
图5-4 符号表
5.4 hello的虚拟地址空间
从5-3节头表可知,地址偏移为0x3a50,使用edb,观察Data Dump
图5-5 各段地址和内容
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
图5-6 hello文件反汇编
hello和hello.o的不同在于hello在链接时进行了重定位,所以二者的虚拟空间地址是不同的。
链接的过程:1.符号解析;2.地址重定位;3.合并重复代码和数据;4.符号解析和重定位的全局优化;5.库的链接。
重定位的实现:重定位将目标文件中的相对地址转换为最终可执行文件或库文件在内存中的绝对地址。重定位的实现分为:重定位记录的生成和重定位记录的应用。
5.6 hello的执行流程
由于我更熟悉gdb的使用,所以以下执行使用gdb实现。
图5-7 调试hello.out
使用命令:i func 查看程序中的所有函数,可以得知程序中的函数有__init、__cxa_finalize、puts、printf、getchar、atoi、exit、sleep、_start、deregister_tm_clones、register_tm_clones、__do_global_dtors_aux、frame_dummy、main、__libc_csu_init、__libc_csu_fini、_fini。地址从0x1000开始。
图5-8 所有函数及其地址
给所有的函数打上断点:
图5-9 函数断点
调试可知,先执行__init函数,然后执行main函数。
5.7 Hello的动态链接分析
1.观察共享库信息,观察到库的名称和版本号
图5-10 共享库信息
2.观察已加载的共享库的地址范围和偏移量
图5-11 共享库的地址范围和偏移量
3.显示程序中所有已加载的全局变量
图5-12 全局变量
5.8 本章小结
链接时的操作是将可重定位目标文件通过符号解析、地址重定位等操作,将程序的变量、程序的地址进行重定位是程序能够跑起来。程序虚拟内存中代码段和数据段的映射也是通过重定位实现。而共享库是在链接时加入到程序库中,所以说链接是程序运行的重要环节。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机中正在运行的程序的实例。它是操作系统进行资源分配和管理的基本单位。每个进程都有自己的独立地址空间、内存、文件描述符、执行上下文和其他相关资源。
进程的作用主要是:保证独立性、资源管理、并发执行、进程间通信、作业控制、实现多任务、保证安全性。
6.2 简述壳Shell-bash的作用与处理流程
壳是一种命令行解释器,它是操作系统和用户之间的接口。它接收用户输入的命令,并将其解释执行或传递给操作系统执行。其中,bash是一种常见的Unix/Linux壳,也是许多Linux系统默认的壳。
作用:1.命令解释和执行;2.环境变量管理;3.脚本编程;4.输入输出重定向;5.条件判断和流程控制
处理流程:1.读取用户输入;2.语法解析;3.命令查找;4.命令执行;5.输出显示;6.循环处理
6.3 Hello的fork进程创建过程
在main函数中调用fork()函数,操作系统会创建一个新的进程,称为子进程。父进程和子进程会继续执行fork函数后的代码,在父进程中fork函数返回子进程的id,该pid大于0。在子进程中fork函数返回0,表示当前进程是子进程,所以fork函数调用一次返回两次。在调用fork()函数是,操作系统会复制父进程的执行上下文和资源给子进程,然后父进程和子进程并发执行各自的代码,从而实现进程的创建和分离。
6.4 Hello的execve过程
当调用execve函数时,操作系统会将当前进程替换为一个新的可执行程序。这个新的可执行程序可以时hello程序的另一个版本或者时完全不同的程序。
execve函数接收三个参数:1.const char *pathname:指定要执行的可执行文件的路径;2.char *const argv[]:包含命令行参数的字符串数组,最后一个元素必须是Null;3.char *const envp[]:包含环境变量的字符串数组,最后一个元素必须是Null。
在调用execve函数之后,操作系统会关闭当前进程的文件描述符,并加载和执行指定的可执行文件。新的可执行文件开始执行时,操作系统会将控制权转移给新的程序代码,并开始执行新程序的main函数或入口点。如果成功调用execve函数并替换为新的可执行程序,原始的hello程序代码不再继续执行。
6.5 Hello的进程执行
1.进程调度的过程:
先根据调度算法从就绪队列中选择一个最优的进程,保存当前正在执行进程的上下文信息,并加载选中进程的上下文信息,将控制权交给选中的进程,使其开始执行,选中进程开始执行其指令,知道发生阻塞、事件片用尽或者主动放弃执行权等情况。
2.用户态与核心态转换:
当进程需要执行一些特权操作时,需要从用户态转换到核心态。用户态和核心态之间的转换时通过出发特殊的指令或异常事件来实现的,在进程切换或进程调度时,操作系统会负责处理用户态和核心态之间的切换,包括保存和回复进程的上下文信息。
6.6 hello的异常与信号处理
1)1.访存异常:当程序尝试访问无效的内存地址(如访问未分配的内存、非法访问等)时,会触发访存异常。这会导致操作系统发送 SIGSEGV信号给进程。
2.浮点异常:当程序执行浮点运算出现异常情况(如除以零)时,会触发浮点异常。这会导致操作系统发送 SIGFPE信号给进程。
3.非法指令异常:当程序尝试执行非法指令(如在当前处理器不支持的指令)时,会触发非法指令异常。这会导致操作系统发送 SIGILL信号给进程。
4.中断信号:当程序接收到来自外部的中断信号时,如用户按下中断键Ctrl+C或其他硬件中断信号时,会触发中断信号。常见的中断信号包括 SIGINT和 SIGTERM等。
5.资源耗尽信号:当程序无法分配到足够的系统资源时,如内存耗尽、文件描述符耗尽等,会触发资源耗尽信号。常见的资源耗尽信号包括 SIGKILL和 SIGABRT等。
对于信号的处理有以下几种方式:1.默认处理:操作系统会根据信号的类型和默认行为来处理异常。2.信号处理函数:程序可以使用信号处理函数来处理特定的信号。;3.忽略信号:程序可以选择忽略某些信号,即不做任何响应。
2)
1.瞎按
图6-1 运行程序之后瞎按
2.回车
图6-2 回车
3.ctrl-z
图6-3 ctrl-z
4.ctrl-c
图6-4 ctrl-c
5.ps
图6-5 ps
6.jobs
图6-6 jobs
7.pstree
图6-7 pstree
8.fg
图6-8 fg
9.kill
图6-9 kill
6.7本章小结
进程与异常处理是保证程序正常运行的必备方法,进程保证程序独立性、资源管理、并发执行、进程间通信、作业控制、实现多任务、保证程序安全性。而异常处理是在接收到异常信号之后进行的各种处理方式,它保证了程序在运行过程中不会对系统造成影响。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址是指程序在执行过程中使用的地址,它对于程序自身的地址空间而言的。在 hello 程序中,逻辑地址用于访问程序的变量、函数、指令等。
2.线性地址是指逻辑地址经过段式内存管理或分页机制转换后得到的地址。在 hello 程序中,如果使用了段式内存管理,逻辑地址首先会被转换为线性地址。
3. 虚拟地址是指操作系统提供给每个进程的地址空间,它是相对于进程而言的。在 hello 程序中,进程使用的地址都是虚拟地址,包括逻辑地址和线性地址。
4. 物理地址是指实际存在于计算机硬件上的内存地址,它是真正用于访问内存的地址。在 hello 程序中,当操作系统将虚拟地址转换为物理地址时,才能真正访问存储在内存中的数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位字段,被称为段选择符,它包含一个13位的索引号和3位硬件细节。通过段选择符的索引号,可以直接在段描述符表中找到对应的段描述符。段描述符提供了关于一个段的信息。全局的段描述符存储在全局段描述符表(GDT)中,而一些局部的描述符,例如每个进程自己的,存储在局部段描述符表(LDT)中。
通过段式管理,操作系统可以对不同的段进行独立的访问控制和保护。每个段都可以具有不同的属性和权限,以实现对内存的灵活管理和保护。段式管理还允许将不同的段映射到不同的物理内存区域,实现虚拟内存和内存分段的功能。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元负责将线性地址转换为物理地址。为了管理和提高效率,线性地址被划分为固定长度的页(page)单位。例如,在一个32位的系统中,线性地址范围最大为4GB,可以将其划分为以4KB为单位的页。整个线性地址空间可以被看作一个大数组,称为页目录,其大小为2的20次方个页。
页目录是由目录项组成的,每个目录项对应着一个页的物理地址。另外,还有一类称为物理页、页框或页帧的页,这些页是物理内存的固定长度管理单位,通常与内存页一一对应。
通过页式管理,操作系统实现了虚拟内存的功能。它将进程的线性地址空间分割成固定大小的页面,并通过页表将这些页面映射到物理内存中的页面。这样,进程可以使用连续的线性地址空间,而不需要实际连续的物理内存。页式管理还提供了内存保护和共享的机制,可以控制每个页面的访问权限,并实现页面的共享和复制。
7.4 TLB与四级页表支持下的VA到PA的变换
本机芯片为intel core i7,i7的MMU使用了4级页表,36位的VPN被分为了4个9位的VPN。变换的过程如下:
首先,根据虚拟地址的格式,确定虚拟地址中各个字段的含义。然后在TLB中进行快速查找,以确定是否已经有该虚拟地址的映射记录。如果在TLB中找到了对应的映射,就可以直接得到物理地址。如果在TLB中未找到对应的映射,那么需要通过页表来获取映射信息。格局虚拟地址中的页表索引,从一级页表开始查找,一级虚拟地址的中间索引对应二级页表的物理地址,最终在四级页表中找到对应的物理页的物理地址,然后将物理页的物理地址与虚拟地址中的页内偏移量相结合,得到最终的物理地址。同时,将该虚拟地址到物理地址的映射记录添加到TLB中,以供后续快速访问。
7.5 三级Cache支持下的物理内存访问
访问过程如下:
首先,CPU从主存(物理内存)请求数据或指令。如果所需数据或指令位于CPU的L1 Cache中,CPU会直接从L1 Cache中获取,完成内存访问。如果所需数据或指令不在L1 Cache中,CPU会继续在L2 Cache中查找。如果L2 Cache中存在所需的数据或指令,CPU会从L2 Cache中获取,并将其复制到L1缓存中,以备将来的访问。如果不在L2中,就继续去L3 Cache中寻找,过程同上。如果所需数据或指令连在L3Cache中也找不到,CPU会发起主存访问请求,从物理内存中读取所需的数据或指令。一旦数据或指令从物理内存中加载到L3 Cache、L2 Cache和L1 Cache中的某一级缓存,CPU会直接从缓存中访问数据或指令,而不需要再次访问主存。
7.6 hello进程fork时的内存映射
在进程fork时,操作系统会创建一个新的子进程,该子进程将会复制父进程的内存映射。父进程的虚拟地址空间被复制到子进程的虚拟地址空间。这意味着子进程将获得与父进程相同的代码段、数据段、堆和栈。父进程的页表中的每个页表项将被复制到子进程的页表中。这样,子进程将具有与父进程相同的虚拟地址到物理地址的映射关系。当父进程或子进程中的任何一个进程尝试修改某个共享的物理内存页时,操作系统会为该进程分配一个新的物理页,并将要修改的数据复制到新的物理页中。这样,父进程和子进程就不再共享该页,每个进程都有自己的独立副本。
7.7 hello进程execve时的内存映射
在进程执行execve系统调用时,操作系统会加载新的可执行文件到当前进程的内存空间,并更新内存映射。进程执行execve系统调用时,会进行新的内存映射,将新的可执行文件加载到进程的虚拟地址空间中,替换原有的内存映射。这样,进程将以新的代码和数据重新开始执行,原有的内存内容将被替换。
7.8 缺页故障与缺页中断处理
如果在页表中未找到有效的映射,表示发生了缺页故障。此时,处理器会触发缺页中断,将控制权转交给操作系统。操作系统收到缺页中断后,会根据缺页故障的情况进行相应的处理。首先,操作系统会分配一个物理内存页面,可以是空闲的物理页面或者通过页面置换算法选择要被替换的页面。操作系统会从磁盘或其他存储介质中加载所需的页面数据到刚分配的物理页面中。然后更新页表,建立新的虚拟地址到物理地址的映射。最后返回控制权给程序,程序再次运行该指令。
7.9动态存储分配管理
动态存储分配管理是计算机系统中用于动态分配和管理内存的一种机制。它允许程序在运行时根据需要申请和释放内存,以满足程序的动态内存需求。堆是用于动态存储分配的一块内存区域。malloc函数可以从堆中申请一块指定大小的内存空间。而栈是用于存储局部变量和函数调用信息的内存区域。栈上的内存分配和释放是由编译器自动管理的,遵循后进先出的原则。
内存释放时可以调用函数free、delete或操作符来释放,以返回给系统,供其他部分使用。
7.10本章小结
程序的代码需要空间存储,程序运行时需要空间存放变量常量和函数,程序运算时候也需要空间来存放临时结果。所以存储空间与程序息息相关,对于程序十分的重要,但是访存时间太长,如果每次都需要去主存中查找数据或指令,那么程序的运行速度就会大大减缓,所以就有了缓存和虚拟内存的概念,缓存和虚拟内存使程序运行的速度大大加快了,这也使得机器运行的整体速度加快,所以虚拟内存是一个十分重要的概念。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
hello.c文件是程序的源文件,是由高级语言c语言书写而成,其中的逻辑实现和函数实现使用的语言接近我们人类的自然语言,这也是离我们程序员最近的语言。经过预处理之后,hello.c变成了hello.i文件,该文件中的汇编代码接近高级语言。经过编译之后,hello.i文件变成了汇编语言文件hello.s文件。汇编之后,汇编器将hello.s汇编成为可重定位二进制文件hello.o。链接之后,链接器hello.o文件变成了可执行文件hello。在运行时,shell通过fork函数和execve函数创建进程,然后分配空间,运行程序,运行结束之后回收程序。
程序在计算机系统中的运行是一个抽象的过程,我们难以真正的去观察到,但是在深入学习了计算机系统这门课之后,我们对于自己所写出来的程序的运行有了更加清晰的了解,也懂得如何去优化和写出更好的程序,这门课对于我们程序员来说是干货满满,十分重要。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
1.hello.i:预处理得到的文本文件
2.hello.s:编译后得到的汇编语言文本文件
3.hello.o:汇编之后的可重定位文本文件
4.hello.out:链接之后的可执行文本文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统(原书第三版) (Computer Systems: A Programmer’s Perspective (3rd Edition))Randal E.bryant/David O’Hallaron
(参考文献0分,缺失 -1分)
更多推荐
所有评论(0)