**
**
本文以一个简单的C程序hello.c为楔子描绘了从预处理一直到I/O管理的计算机的内部原理,将计算机对程序的处理过程生动地展现在人们眼前。
关键词:hello、程序、P2P、O2O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在Ubuntu下预处理的命令 - 5 -
2.3 Hello的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理 - 11 -
7.1 hello的存储器地址空间 - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级Cache支持下的物理内存访问 - 11 -
7.6 hello进程fork时的内存映射 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理 - 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -
**
**
第一步,向电脑输入代码并存储成hello.c文件。
第二步,将文件中的代码通过预处理形成hello.i、编译形成hello.s、汇编形成hello.o,然后链接使之成为可执行目标文件。
第三步,shell接收用户命令并使用fork函数创建子进程,接着在子进程中使用execve函数将hello.c加载到内存中、使用mmap函数将hello.c文件映射进内存并实现进程间共享内存,同时生成代码段、数据段以及堆栈段,调度器为hello.c文件分配时间片。
第四步,CPU通过生成一个虚拟地址访问主存,同时,CPU中的TLB、4级页表以及CPU和主存之间的三级Cache加速访问内存。在MMU中进行地址翻译之后,虚拟地址转换为物理地址,然后可以从内存中取指、译码并执行。中间使用了众多信号处理命令和函数。
经过以上四步hello由program转为process。
最后,在程序执行完毕之后,shell回收子进程,又变为未执行hello之前,即020。
Codeblocks 2017,VMware,ubunto,edb,gdb,gcc
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件 作用
Hello.c 源程序文件
Hello.i 源代码经过预处理后得到的ascii码文件
Hello.s .i文件经过编译得到的汇编代码文件
Hello.o 汇编后的二进制目标文件
本章主要描述了如何将一个.c文件经过处理后打印到屏幕上供用户观看的大致过程,其中涉及到了计算机内十分重要的几个硬件——CPU、RAM、IO等等,向我们展示了计算机的工作是如何进行的。
(第1章0.5分)
概念:在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
作用:扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。
首先,在hello.i文件中插入了所有用#include命令指定的文件,包括stdlib.h、stdio.h、unistd.h三个文件。其次,删除了注释等信息。
本章主要讲述预处理这一执行源程序的第一个步骤。通过观察.c文件经预处理后得到的.i文件理解了预处理的作用——扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。
(第2章0.5分)
概念:把高级语言变成计算机可以识别的2进制语言。
作用:产生汇编代码。
常量:以下常量存放在只读数据域中
全局变量:存放在.data段,hello中无全局变量
局部变量:存储在堆栈段,如当前代码中的i,argv,argc
如图中的-20(%rbp)中存的就是argc
图中存的是i
对argv数组进行操作,数组在存储空间中申请的是连续的内存。
终止程序
从键盘输入内读取字符串
打印到屏幕
字符串转为整型
休眠
清除回车
本章具体分析了编译(由.i到.s)的过程,对生成的汇编代码进行分析,了解到了C语言的数据在汇编代码中的存储位置的不同以及操作在汇编代码中的实现。
(第3章2分)
概念:可以被CPU直截执行的编程语言
作用:生成二进制目标代码文件
ELF格式
ELF头
段头部表
.init
.text
.rodata
.data
.bss
.symtab
.debug
.line
.strtab
节头部表
机器语言的构成:由机器指令集构成。
与汇编语言的映射关系:一一对应。
操作数的不一致:
前者是hello.o的反汇编代码,后者是hello.s。能明显看出前者的操作数是16进制,而后者是10进制。
分支转移函数调用:
hello.o中,跳转位置不再是hello.s中的符号,而是地址。同时给出地址偏移量。
本章主要分析了汇编(.s到.o)的过程、.s与.o的主要不同点——操作数的不一致以及分支转移函数调用处的参数的不同。
(第4章1分)
概念:将各种代码和数据片段收集并组合成为一个单一文件的过程。
作用:使得生成的文件可以被加载到内存并执行。
ELF格式
ELF头
段头部表
.init
.text
.rodata
.data
.bss
.symtab
.debug
.line
.strtab
节头部表
.data段:
一、hello与hello.o的不同
1、hello中加入了许多除.text外的节,而hello.o中只有.text节
2、hello由于与动态链接库中的库函数链接上,故比hello.o多了许多函数。
二、链接的过程
符号解析、重定位。
三、重定位
第一步,重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。
第二步,重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
本章具体分析了链接的作用——将各种代码和数据片段收集并组合成为一个单一文件,链接的过程——符号解析和重定位。深入理解了重定位的过程以及作用。了解了可执行目标文件与.o文件的反汇编代码的不同。
(第5章1分)
概念:一个执行中程序的实例。
作用:每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。
作用:接收用户命令,然后调用相应的应用程序。
处理流程:
1.从脚本或终端或bash -c选项后的字符串中获取输入
2.将获取的输入分解成词元,此步骤会执行别名展开
3.将词解析为简单命令或复合命令
4.执行各种shell展开
5.执行必要的重定向,
6.执行命令
如果命令中包含“/”,则执行制定路径的程序;如果命令中不包含“/”,会检查是否是shell函数,shell内建命令,如果都不是,则在PATH环境变量中的路径进行查找。
7.等待命令结束获取命令执行状态
子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
execve函数在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve才会返回到调用程序。
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。
内核为每个进程维持一个上下文。它由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策叫做调度。
处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
异常种类:
1、来自键盘的终止进程:SIGINT
2、来自键盘的挂起进程:SIGSTP
命令的运行:
本章具体分析了进程管理,理解了fork函数以及execve函数在进程执行过程中的作用。同时通过运行进程时输入的各种命令——ps、pstree、crtl+c、crtl+z、kill、fg等等了解了进程管理。
(第6章1分)
逻辑地址:访问指令给出的地址。
线性地址:段地址+偏移地址。
虚拟地址:程序访问存储器所使用的逻辑地址。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址。
实模式下: 逻辑地址CS:EA =物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,
段地址+偏移地址=线性地址。
分页管理机制通过上述页目录表和页表实现32位线性地址到32位物理地址的转换。控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位(即位22至位31)作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位(即位12至位21)作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。
1、CPU产生一个虚拟地址,并把它传送给MMU。
2、MMU生成PTE地址,并从告诉缓存/主存请求得到它。
3、高速缓存/主存向MMU返回PTE。
4、MMU构造物理地址,并把它传送给高速缓存/主存。
5、高速缓存/主存返回所请求的数据字给处理器。
CPU产生一个虚拟地址并发送给MMU,如果命中则发送给一级Cache,如果没有命中则回页表中查找,然后一级一级重复上述步骤。
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
1、删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2、映射私有区域。为新程序的代码、数据、bss和栈区创建新的区域结构
3、映射共享区域。
4、设置程序计数器,使之指向代码区域的入口点。
1、搜索区域结构的链表,把虚拟地址和每个区域结构中的vm_start和vm_end作比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。
2、判断进程是否有读、写或者执行这个区域内页面的权限。如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
3、到这一步,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。此时,缺页处理程序选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送虚拟地址到MMU。
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的 块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。
函数原型:void *malloc(size_t size)
成功:
返回已分配块的指针,块大小至少 size 字节,对齐方式依赖编译模式:8字节(32位模式),16字节(64位模式)
If size == 0, returns NULL
出错:返回 NULL (0) ,同时设置 errno
目标:最大化吞吐量,最大化内存利用率
策略:利用首次适配、下一次适配和最佳适配放置空闲块;分割空闲块;合并空闲块。
本章具体分析了虚拟内存管理。在地址层面,由逻辑地址开始,首先转为线性地址,再转为虚拟地址,最后转为物理地址,再通过TLB、三级cache等对上述过程进行加速形成了完整的计算机地址翻译。同时,大致了解了动态内存分配的方法与策略。
(第7章 2分)
设备的模型化:文件
设备管理:unix io接口
1、打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2、Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件
3、改变当前的文件位置。对于每个打开的文件内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k
4、读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处没有明确的“EOF符号”。
5、关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数:
1、open函数可以打开或者创建一个文件,函数原型如下
#include
int open(const char *pathname,int oflag,…/mode_t mode/)
返回值:若成功返回文件描述符,若出错返回-1
pathname不用说指的是文件的路径名,注意使用’/'来间隔目录。
对于第二个参数oflag,以下是需要使用到fcntl.h中的常用参数:
O_RDONLY(只读)
O_WRONLY(只写)
O_RDWR(读写)
//以上这三个常量只能选择一个
O_APPEND(末尾追加)
O_CREAT(如果文件不存在则创建文件,这时需要使用第三个参数mode,指定该文件的访问权限位)
O_EXCL(如果文件存在,则出错,用来检测文件是否存在)
O_TRUNC(如果文件存在,而且为只读或只写打开,则将其长度截短为1)
对于open函数,当且仅当创建新文件时才使用的三个参数。由open返回的文件描述符一定是最小的未用的文件描述符,这可以用先关闭标准输出,然后打开文件,则文件的描述符为1。
2、creat函数
creat函数用来创建一个新文件,函数原型如下
#includ
int creat(const *pathname,mode_t mode)
返回值:若成功则返回为只写打开的文件描述符,否则返回-1
等效于open(pathname.O_WRONLY | O_CREAT | O_TRUNC,mode)
由于creat只能以写的方式打开文件,要想读取该文件需要creat,close,open。可以直接采用如下方式open(pathname,O_RDWR | O_CREAT |O_TRUNC,mode)。
3、close函数
关闭文件并释放其所占资源,函数原型如下
#include
int close(int filedes)
返回值:成功返回0,否则返回-1
4、lseek函数
每个打开的文件在内核中都维护了一个“当前文件偏移量”,指的是从文件开始处计算的字节数。除非使用O_APPEND,否则偏移量一般都为0。
调用lseek显示地为一个打开的文件设置偏移量,函数原型如下
#include
off_t lssek(int filedes, off_t offset, int whence)
返回值:若成功则返回新的文件偏移量,若出错则返回-1
//若whence为SEEK_SET,则将文件的偏移量设置为距文件开始处offset个字节。////
//若whence为SEEK_CUR,则将文件的偏移量设置为当前值加offset,offset可正可负。
//若whence为SEEK_END,,则将文件的偏移量设置为文件长度加offse,offset可正可负,
//注意文件偏移量可以大于当前文件长度,系统并不会为之间的空洞分配磁盘空间,只为新的数据分配磁盘空间。
5、read函数
调用read函数从打开的文件中读取数据,函数原型如下:
#include
ssize_t read(int filedes,void *buf,size_t nbytes)
返回值:若成功返回读到的字节数,若已到文件结尾则返回0,若出错返回-1
6、write函数
调用write函数向打开的文件写数据,函数原型如下:
#include
ssize_t write(int filedes,const void* buff,size_t nbytes)
返回值:若成功返回已写的字节数,若出错返回-1
其返回值通常与参数nbytes相同,否则表示出错。常见的出错原因包括:磁盘已经写满、超过了文件的最大长度限制。对于普通文件,写操作从文件的当前偏移量处开始。执行一次成功的写之后,文件的偏移量增加实际写的字节数。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
本章大致说明了linux系统下的I/O管理,包括文件操作函数、printf函数以及getchar函数。
(第8章1分)
第一步,向电脑输入代码并存储成hello.c文件。
第二步,将文件中的代码通过预处理形成hello.i、编译形成hello.s、汇编形成hello.o,然后链接使之成为可执行目标文件。
第三步,shell接收用户命令并使用fork函数创建子进程,接着在子进程中使用execve函数将hello.c加载到内存中、使用mmap函数将hello.c文件映射进内存并实现进程间共享内存,同时生成代码段、数据段以及堆栈段,调度器为hello.c文件分配时间片。
第四步,CPU通过生成一个虚拟地址访问主存,同时,CPU中的TLB、4级页表以及CPU和主存之间的三级Cache加速访问内存。在MMU中进行地址翻译之后,虚拟地址转换为物理地址,然后可以从内存中取指、译码并执行。中间使用了众多信号处理命令和函数。
经过以上四步hello由program转为process。
最后,在程序执行完毕之后,shell回收子进程,又变为未执行hello之前,即020。
学习完这门课,我最深刻的感悟就是自己的渺小无知、自己对于计算机的一知半解。因此,我要更加努力地学习课程,希望有朝一日能够真正做到“深入”理解计算机。
(结论0分,缺失 -1分,根据内容酌情加分)
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
文件 作用
hello.c 源程序文件
hello.i 源代码经过预处理后得到的ascii码文件
hello.s .i文件经过编译得到的汇编代码文件
hello.o 汇编后的二进制目标文件
hello 链接产生的文件
hello.txt hello的反汇编代码文件
Helloo.txt Hello.o的反汇编代码文件
为完成本次大作业你翻阅的书籍与网站等
[1] https://www.xuebuyuan.com/3228274.html
[2] https://www.cnblogs.com/pianist/p/3315801.html
[3] https://blog.csdn.net/yeahwell/article/details/8741178
[4] https://blog.csdn.net/yeahwell/article/details/8719123
[5] https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)