程序人生-计算机系统大作业

摘  要

这篇是深入理解计算机系统这门课的一个总结,从hello.c这一程序从出生到消亡的全过程来概括计算机系统对一般程序的处理过程。本文详细讲述了各个步骤中的要点,比较相似步骤之间的异同,并从中探讨计算机系统处理程序的逻辑。

关键词:编译系统;进程;linux;shell                           

第1章 概述

1.1 Hello简介

1、P2P(Program to process)

Hello程序从hello.c代码开始,经过预处理、编译、汇编、链接,生成可执行目标文件,再被fork创建子进程、execve写进上下文,最终成为一个进程。

编译系统各步骤及其生成的文件如下图所示:

                                   图1 编译系统

2、020(Zero to zero)

Hello程序要经历一个从无到有,再从有到无的过程。

从无到有就是指程序员编写出hello.c这一文件的过程,而经过编译系统和操作系统的P2P操作,hello进程被创建并运行,而shell进程已经写好了wait函数等待回收这一进程资源,包括进程号、主存上分配的地址空间,在hello进程在屏幕上输出Hello World之后,hello程序就被回收掉了,此为从有到无。

1.2 环境与工具

硬件环境:Intel Core i7-10870H CPU, 2.21GHz,x64,16GB RAM

软件环境:Windows11,VMware17,Ubuntu22.04

开发工具:vim,edb,gcc

1.3 中间结果

hello.c :hello代码

hello.i :预处理之后的源程序

hello.s :汇编文件

hello.o :可重定位的目标文件

hello :可执行目标程序

asm.txt :反汇编文件

1.4 本章小结

本章概述了hello程序的整个运行过程,列出了实验所用的软硬件环境和开发工具。

第2章 预处理

2.1 预处理的概念与作用

预处理指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。是把*.c变成*.i。

作用是把源程序中以#开头的行中引用的各种外部文件通过typedef、extern等方式引入程序文件。

2.2在Ubuntu下预处理的命令

预处理的命令行如下:

                          图2 预处理命令

2.3 Hello的预处理结果解析

运行完预处理命令,文件夹里多了一个hello.i文件:

                         图3 预处理结果

打开来看一看:

                                                             图4 hello.i

可以看到,原来hello.c的代码被放在了最后面,注释被删掉了,前面是从外部库引入的函数和类型定义,举例如下:

#include <stdio.h>,stdio.h中的scanf函数引入:

                            图5 scanf引入

几个类型的定义如下:

                            图6 几个类型

2.4 本章小结

本章介绍了预处理的Linux命令行,对预处理进行的操作和得到的文件hello.i与源文件hello.c的关系进行了分析。

第3章 编译

3.1 编译的概念与作用

编译是指把预处理得到的*.i转变为*.s,即汇编语言程序。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

作用是把高级语言程序转为汇编指令,便于后续翻译成机器指令。

3.2 在Ubuntu下编译的命令

                            图7 编译指令

执行完,文件夹出现了hello.s文件。

                            图8 编译结果

3.3 Hello的编译结果解析

Hello.s全文如下:

 

                          图9 hello.s

3.3.1.

变量:

参数argc存放在-20(%rbp)的位置,argv存放在-32(%rbp)的位置。局部变量i存放在-4(%rbp)的位置。

常量:

报错用的printf的字符串存放在.LC0标记后的.string段,

                            图10 printf第一次调用

输出信息用的printf的字符串存放在.LC1标记后的.string段,

                            图11 printf第二次调用

给这个printf函数传参,用%rdi传递字符串常量,用%rsi传递原本在栈里的argv所指向的argv[1]。

3.3.2.

赋值:

                            图12 i的赋值

如图,i被赋初值0。

3.3.3.

算术操作:

for循环中出现的i++,在汇编语言文件中是这样写的,

                            图13

3.3.4.

关系操作:

判断argc!=4,汇编语言文件是这样做的,

                            图14

逻辑是相等则跳转到.L2,不相等则顺序往下。

还有一处是for循环的终止条件,实现如下,

                            图15

C代码中,用的条件是i<8,而汇编语言则用了i<=7。满足则跳转.L4继续循环,不满足则向下顺序执行跳出循环。

3.3.5.

数组操作:

由于argv指向的是一个数组。读取argv[2]、argv[3]分别用了如下操作,

                            图16

逻辑就是先把argv[0]的地址(就是argv)放进%rax寄存器,在此基础上加上想要读取的数组成员的地址偏移量,最后把%rax内容所指向的内容存进%rax,就完成了数组操作。

3.3.6.

控制转移:

如3.3.4中for循环终止条件所描述,

                     图17

用i<=7,作为判断条件,满足则跳转.L4继续循环,不满足则向下顺序执行跳出循环。

3.3.7.

函数操作:

调用了printf函数,传参方法如3.3.1所描述。

调用了atoi函数,

                            图18

传参用的是%rdi寄存器,传了一个argv[3],返回值又被传给了函数sleep,用的还是%rdi寄存器。

                            图19

调用了getchar函数,无参,仅作为暂停使用,返回值被忽略。

                            图20

3.4 本章小结

本章描述了从*.i文件到*.s文件所使用的命令行,分析了编译器是以怎样的逻辑编写汇编语言的。

第4章 汇编

4.1 汇编的概念与作用

汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。

作用:把汇编程序翻译成机器语言程序(可重定位目标文件)。

4.2 在Ubuntu下汇编的命令

命令行和执行后的文件夹内容如下图所示。

                            图21 汇编指令

4.3 可重定位目标elf格式

    可重定位目标文件由下列节构成:

.text:已编译程序的机器代码

.rodata:只读数据

.data:已初始化的全局和静态C变量

.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量

.symtab:在程序中定义和引用的函数和全局变量信息

.rel.text:.text节重定位信息

.rel.data:被模块引用或定义的所有全局变量的重定位信息

.debug:调试符号表

.line:行号

.strlab:.symtab和.debug节中的符号表

    ELF头:

                            图22

    节头表:

                            图23

    .rel.text节:

                            图24

    .symtab节:

                            图25

4.4 Hello.o的结果解析

objdump -d -r hello.o得到反汇编如下:

                        图26 hello.o反汇编

与hello.s相比,这个文本每条指令都有了十六进制地址,而hello.s文件只是指令没有地址;

这个文本没有了.L0等标记信息,而是通过PC增减的方式实现跳转;

这个文本的操作数都是十六进制表示,有别于hello.s中的十进制;

这个文本汇编语言指令与机器指令是一一对应的双射关系。

4.5 本章小结

本章描述了从*.s文件到*.o文件所使用的命令行,分析了汇编器是怎么处理汇编语言文件的,观察了ELF文件格式,比较了经过汇编器处理的文件和之前的文件的区别。

第5章 链接

5.1 链接的概念与作用

链接是指把用到的零散的代码片段合成一个文件。

作用:通过链接,使得分离编译成为可能;动态绑定可使定义、实现、使用分离。

注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

                            图27 链接指令

5.3 可执行目标文件hello的格式

    ELF头:

                     图28

    节头表:

                            图29

                            图30

    程序头表:

                            图31

    符号表:

                            图32

5.4 hello的虚拟地址空间

       

                            图33

5.5 链接的重定位过程分析

                             图34 hello.out反汇编

    可以看出,这段文本增加了所使用的其他库中的函数的代码,体现了链接的工作。Call的内容都是地址,而且增加了*.out文件特有的init节。

5.6 本章小结

本章描述了hello.o到hello.out的过程,分析了链接器的工作内容。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

进程的作用:提高CPU的执行效率,减少因为程序等待带来的CPU空转以及其他计算机软硬件资源的浪费。

6.2 简述壳Shell-bash的作用与处理流程

处理流程:

  • 读取从键盘输入的命令;
  • 判断命令是否正确,且将命令行的参数改造为系统调用execve()内部处理所要求的形式;
  • 终端进程调用fork()来创建子进程,自身则用系统调用wait()等待子进程完成;
  • 当子进程运行时,它调用execve() 根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令;
  • 如果命令行末尾有后台命令符号&,终端进程不执行等待系统调用,而是立即发提示符,让用户输入下一条命令;如果命令末尾没有&,终端进程要一直等待。当子进程完成处理后,向父进程报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。

6.3 Hello的fork进程创建过程

终端调用fork函数,创建一个新的进程,父进程返回子进程PID,子进程返回0子进程得到与父进程相同的代码和数据段、堆、共享库、用户栈,但物理地址是新的。子进程和父进程PID不同,互不影响。

6.4 Hello的execve过程

创建的子进程调用execve函数,不返回,把hello进程的envp和argc、argv写进上下文。

6.5 Hello的进程执行

用户程序之间的切换是由内核完成的,内核对所有资源有同等权限。想要执行hello,要给他分配时间片,当进程出现异常或时间片用完,内核就会进行上下文切换,避免浪费资源。

6.6 hello的异常与信号处理

刚刚运行的时候会发生缺页异常,这是故障的一种,特点是如果处理程序成功完成了故障处理,会返回引起故障的指令,而不是下一条。

乱按:

                            图35

    可以看出终端显示输入流和hello是两个并发进程,没发信号就互不干扰。

    按下Ctrl+Z之后ps、jobs、pstree

                            图36

Ctrl+Z会让内核给hello进程发送3号SIGTSTP信号,进程暂时挂起,但仍然存在。执行命令fg可以重新唤起hello。

                            图37

Ctrl+C:

                            图38

这一命令会让内核给hello发2号信号SIGINT,这样该进程就被终止并回收,不复存在了。

Kill:

                            图39

可以看出,在进程挂起的时候,也可以通过kill命令发送SIGINT信号将其终止。

6.7本章小结

本章描述了hello作为一个进程的创建、挂起、唤起、终止的过程和其中的信号处理机制。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:从程序的角度看到的内存单元和存储单元地址,一般是偏移地址,就是hello反汇编得到的地址数据。

线性地址:将逻辑地址段号和偏移量运算后得到的地址。

虚拟地址:虚拟存储器里的地址,要比物理地址大很多,可以映射到物理地址。

物理地址:真正的存储地址,也就是hello真正在主存占据的位置。

7.2 Intel逻辑地址到线性地址的变换-段式管理

将地址分为代码段、数据段,逻辑地址就可以由段号和段内偏移量构成。

                           图40 逻辑地址

有特点如下:

  • 段式管理以段为单位分配内存,每段分配一个连续的内存区。
  • 由于各段长度不等,所以这些存储区的大小不一。
  • 同一进程包含的各段之间不要求连续。

④ 段式管理的内存分配与释放在作业或进程的执行过程中动态进行。

7.3 Hello的线性地址到物理地址的变换-页式管理

把主存和虚存都分成同样大小的页面,每个页面是否命中由页表来记载,为了提高页表使用效率,页表有自己的缓存TLB。

页命中完全由硬件实现,缺页处理由软硬件共同实现。地址翻译流程如下:

①  处理器生成一个虚拟地址,并将其传送给MMU。

  •  MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE。
  •  高速缓存/主存向MMU返回PTE。
  •  MMU 将物理地址传送给高速缓存/主存。

⑤  高速缓存/主存返回所请求的数据字给处理器。

7.4 TLB与四级页表支持下的VA到PA的变换

使用多级页表是为了缩短单个地址的位数,防止由于过大的虚拟地址空间而使得虚拟地址变得太长。

四级页表结构如图所示:

                            图41 四级页表

7.5 三级Cache支持下的物理内存访问

三级Cache结构如图:

                                  图42 三级cache

7.6 hello进程fork时的内存映射

终端调用fork函数之后,子进程就有了自己的PID和地址空间,和父进程共享库、用相同的段,整体是父进程的一个副本。

7.7 hello进程execve时的内存映射

地址不变,内容改变,这个时候他就成为了与父进程区分开的另一个进程。

7.8 缺页故障与缺页中断处理

发生缺页故障,处理程序唤起对应的进程,调页结束再执行引发缺页故障的这一条指令。

7.9本章小结

本章分析了hello对内存的使用方法。

 结论

    在读大学之前,甚至说是学习计算机系统这门课之前,我对程序运行的理解都是极其肤浅的,认为程序能跑通是理所当然。

现在学了这门课,我明白了没有任何事是本该如此的,就一个简单的hello程序,里面都蕴含了计算机科学家的智慧。我认为,深入理解计算机系统这门课,可能是我正确认识计算机的一个开始。

从这次大作业中,我学到了:对待计算机、对待科学、对待人生,一定要有严谨的态度。一步出错,连最简单的hello都会是一个难题,更不必说我们要学的复杂的知识体系,万万不可懈怠。

附件

hello.c(初始的程序代码)

hello.i(预处理后的代码)

hello.s(汇编语言文件)

hello.o(可重定位目标文件)

hello(可执行目标文件)

参考文献

[1]  https://blog.csdn.net/forcj/article/details/117967945

[2]  https://my.oschina.net/mikeowen/blog/4812888

[3]  https://blog.csdn.net/goJiaWei123/article/details/98862886

[4] https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80/3283849?fr=aladdin#3

[5]  https://www.cnblogs.com/mengxiaoleng/p/11921912.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值