hello的一生

计算机科学与技术学院
2019年12月
摘 要

关键词:预处理;编译;汇编;进程;IO设备;存储管理
本文介绍了hello.c文件在linux中如何生成和执行的过程,hello文件将会经过预处理,编译,汇编,链接等过程生成可执行文件。然后再操作系统的管理下再shell上进行执行,生成子进程和加载hello程序,然后会为其分配地址空间和内存分配,以及hello执行的过程中与操作系统的接口调用。Hello看似简单的一生,却隐含着许许多多复杂的机制,为我们呈现了一个精彩而有趣的程序底层流程。

目 录

第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 -

第1章 概述
1.1 Hello简介
编写完成源文件后将通过预处理器cpp将头文件的内容预处理得到hello.i文件,经过ccl编译器可以得到汇编程序,然后通过as汇编器能够得到可重定位程序的二进制文件,通过ld连接器与外部库进行链接,得到可执行文件hello,然后再命令行中执行./hello命令shell判断不是内置命令,所以fork一个子进程,再子进程中execve一个hello程序,然后为其分配虚拟空间,然后将会通过IO接口得到键盘输入的信息,以及经过异常处理和IO接口得到屏幕输出,最后会回收进程。
1.2 环境与工具
X64 CPU;3GHz;16G RAM;256GHD Disk 以上
Windows10 64位; Vmware 11以上;Ubuntu 16.04 LTS 64位
Gedit,GCC,visualstudio
1.3 中间结果
Hello.c 源文件
Hello.i 源文件预处理过后的文件
Hello.s 预处理后的文件编译之后的文件
Hello.o 汇编之后的可重定位的文件
Hello.elf hello.o的全部elf信息
Helloexe.elf hello的全部elf信息
Hello 链接生成的可执行的文件
Hello1.s 可执行文件反汇编生成的程序
Hello2.s hello.o反汇编得到的程序
1.4 本章小结
概括了hello生成的过程,简单讲述了代码如何经过预处理,编译,汇编,链接生成程序并在execve,创建虚拟地址空间,执行目标文件,回收进程等在系统中运行的过程。

第2章 预处理
2.1 预处理的概念与作用
(以下格式自行编排,编辑时删除)
预处理是指在编译之前进行的处理。
C语言的预处理主要有三个方面的内容:

  1. 宏定义
  2. 文件包含
  3. 条件编译
    其中宏定义又称为宏代换、宏替代。
    格式:#define 需要替换的格式串 目标格式串
    例如:#define PI 3.14
    在预处理时会将PI标识为3.14。

其中文件包含指的是一个文件包含另一个文件的内容
格式:#include “文件名”
例如:#include<stdio.h>
预处理时,一个头文件将会将该文件包含至源文件中

其中条件编译指的是希望语句在满足条件时才进行编译
格式(1):
#ifdef 标识符
语句1
#else
语句2
#endif
只有当标识符已经定义时,语句1才满足编译的条件才会参加编译
此类语句有#if/#ifdef/#ifndef/#else/#elif/#endif等
2.2在Ubuntu下预处理的命令

图2.2.1
如图2.2.1预处理命令为 gcc 文件名 -E -o hello.i
其中使用gcc命令时有四个编译选项
1.-E预处理,主要进行宏展开
2.-S编译,生成汇编代码,生成.s文件
3.-c汇编,生成机器码,生成.o文件
4.-o链接,生成可执行文件

此时我们使用命令
Gcc hello.c -E -o hello.i
将生成如下hello.i文件如图2.2.2,我们在visualstudio中将其打开,能看到其中有包含的一系列复杂的头文件的命令例如stdio.h,featuers.h等头文件

图2.2.2
2.3 Hello的预处理结果解析
如图2.3,在hello.i中我们可以发现原先的代码被扩展成了3042行,其中3029行开始时是原先的代码,在hello.i中我们可以发展,原先的代码中含有的#inlucde<stdio.h>和#include<stdlib.h>在hello.i的源代码中均消失不见,但是当我们观察源代码之前的3000多行后,我们可以发现在代码之中,原来#include包含的头文件均被展开为更加复杂的定义和函数。

图2.3
2.4 本章小结
1.预处理时所有的头文件插入文本中
2.所有宏定义转化为具体的值
生成ascii码的文本文件

第3章 编译
3.1 编译的概念与作用
编译,就是把代码转化为汇编指令的过程,汇编指令只是CPU相关的,也就是说C代码和python代码,代码逻辑如果相同,编译完的结果其实是一样的。
注意:这儿的编译是指从 hello.i 到 hello.s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

图3.2
其中使用gcc命令时有四个编译选项
1.-E预处理,主要进行宏展开
2.-S编译,生成汇编代码,生成.s文件
3.-c汇编,生成机器码,生成.o文件
4.-o链接,生成可执行文件
因此此编译命令为gcc 文件名.i -S -o hello.s
Gcc hello.i -S -o hello.s,如图3.2

3.3 Hello的编译结果解析
以下是通过gcc hello.i -S -o hello.s 编译而成的汇编代码,如图3.3.1和3.3.2

图3.3.1

图3.3.2
(以下格式自行编排,编辑时删除)
3.31 全局函数
其中

图3.3.3
如图3.3.3
.file “ hello.c”表示的是该文件的名字是hello.c
.text 表示的是代码节
.section .rodata表示的是只读数据节
.align8 表示的是8字节对齐
.LC0:
.string “\347\224\250\346\263\255:Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201”表示的是pirntf中的字符串"用法: Hello 学号 姓名 秒数!\n"字符串
.LC1:
.string “hello %s %s\n” 表示printf中的字符串"Hello %s %s\n"
.text 表示代码
.global main表示全局函数
3.3.2主函数main解析

图3.3.4
如图3.3.4
其中mian有两个参数,分别是int argc,char *argv[]
其中.cfi开头的是call frame information的操作,可以忽略
Pushq %rbp//压入rbp
Movq %rsp,%rbp//将%rbp的值给%rsp
Subq $32 ,%rsp//构造栈空间
由于传入参数的顺序为%rdi,%rsi
Movl %edi,-20(%rbp)//参数argc的位置在%rbp-0x20
Movl %rsi,-32(%rbp)//参数argv[]的位置在%rbp-0x32
3.33条件判断
Cmpl $4,-20(%rbp)//将%rbp-0x20,即参数argc与$4进行比较
je .L2//当相同时跳转至.L2代码处
leaq .LC0(%rip),%rdi//将%rip+.LCO处的字符串赋值给%rdi,为调用pus函数做准备
call puts@PLT//调用puts函数,在源c代码中表示为printf函数
movl $1,%edi//将1赋值给%edi,为调用exit函数做参数
call exit@PLT//调用exit函数,退出程序。

3.34条件判断的另一部分

图3.3.5
如图3.3.5
其中跳转至.L2后
.L2:
Movl $0,-4(%rbp)//将%rbp-0x4处即int i的值赋值为0
Jmp .L3//跳转到.L3代码处
3.34for循环汇编
.L3:
Cmpl $7,-4(%rbp)//将%rbp-0x4处即int i的值与7比较
Jle .L4//如果小于等于则跳转到.L4代码处
Call getchar@PLT//调用getchar函数
movl $0, %eax//将返回值赋值为0
leave// leave指令将EBP寄存器的内容复制到ESP寄存器中,以释放分配给该过程的所有堆栈空间。然后,它从堆栈恢复EBP寄存器的旧值
ret//返回主函数值

.L4
movq -32(%rbp), %rax//将%rbp-0x32处的值即argv[0]的首地址赋值给%rax
addq $16, %rax//%rax = %rax +16,即argv[2]的值
movq (%rax), %rdx//将%rdx赋值为argv[2]的值
movq -32(%rbp), %rax//将%rbp-0x32处的值即argv[0]的首地址赋值给%rax
addq $8, %rax//%rax = %rax +8,即argv[1]的值
movq (%rax), %rax//将%rax赋值为%rax解引用之后的值
movq %rax, %rsi//将%rdx赋值为argv[1]的值
leaq .LC1(%rip), %rdi//将%rip+.LC1处的值赋值给%rdi
movl $0, %eax//%eax = 0
call printf@PLT//调用printf函数,其中参数为%rdi,%rsi,%rdx
movq -32(%rbp), %rax//将%rax赋值为%rbp-0x32处的值
addq $24, %rax//%rax = %rax+4
movq (%rax), %rax//%rax赋值%rax处解引用后的值
movq %rax, %rdi//%rdi = %rax,作为atoi的参数
call atoi@PLT//调用atoi函数使字符串转化为integer类型的数据
movl %eax, %edi//将%edi = %eax,%eax是atoi函数的返回值
call sleep@PLT//调用sleep函数,参数为%edi
addl $1, -4(%rbp)//(%rbp-4) = (%rbp -4) + 1//将%rbp-4处的值,即i的值加1进入循环。

3.4 本章小结
本章说明了编译器的工作机制,说明如何将hello.i转化成hello.s。其中将原来的代码扩展为更加复杂,但是中间指令的汇编语言。

第4章 汇编
4.1 汇编的概念与作用
汇编程序是把由用户编制的汇编程序翻译为机器语言的一种系统程序。
通常采用两遍扫描源程序的算法。第一遍扫描源程序根据符号的定义和使用,收集符号的有关信息到符号表中;第二遍利用第一遍收集的符号信息,将源程序中的符号化指令逐条翻译为相应的机器指令。
4.2 在Ubuntu下汇编的命令

图4.2
命令格式为gcc 文件名.s -c -o hello.o
如图4.2,因此命令为gcc hello.s -c -o hello.o
4.3 可重定位目标elf格式
使用readelf -h hello.o可以查看文件头信息。

图4.3.1
如图4.3.1,在文件头中的带节点表的信息和重定位的分析,这个图片显示了该文件的信息有小端序,系统架构x86-64,头本的大小64字节,节头的大小64字节,节头数量13,字符串表索引节头12.

使用readelf -S hello.o能够得到节头的信息

图4.3.2
如图4.3.2时各节头信息共有13个节头,从偏移量0x480开始,同时也能得到各节头的带下,类型,地址信息和对齐信息。

使用readelf -s hello.o 能够得到hello.o的符号表信息

图4.3.3
在.symtab符号表中有16个符号
如图4.3.3其中可以看到main,puts,exit,printf,atoi,sleep,getchar均作为强符号出现,在解析的时候会优先使用。

4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
使用命令objdump -d -r hello.o 得到hello.o的汇编代码

图4.4

1.与hello.s的汇编语言进行对比可以发现在分支结构中.s文件使用的是段名称而反汇编代码中使用的是具体地址
2.在.s文件中跳转实现的均为使用.L1,.L2,而在objdump之后的文件中可以发现,此时有了具体的地址和偏移量。
3. 同时.s文件中调用函数使用的是printf@PLT之类的函数名字,而objdudmp之中将保存的是main函数地址及其偏移量,例如<main+0x87>。
4. 同时全局变量也获得了新的地址,不再是%rip+0x32之类的偏移量表示的地址数据。
4.5 本章小结
Hello.s和hello.o的汇编和反汇编文件的操作函数及其功能上一致,但是在具体的跳转过程中,代码执行的过程中,hello.s的文件均为相应的偏移量和基地址,而hello.o的反汇编文件中出现了具体地址。
在反汇编的过程中,将计算出偏移量和具体地址,提高了栈空间的利用效率,但是在事实上,hello.s和hello.o的代码文件是具有一一映射的关系。

第5章 链接
5.1 链接的概念与作用
链接指的是将各种代码和数据段收集并链接成一个单一可执行文件的过程,链接包括两个步骤符号解析和重定位。

  1. 符号解析:在符号解析的过程中,连接器确定每一个符号和对应的强弱关系,选择强符号和弱符号,并选择优先解析的符号,例如有一个强符号和多个弱符号的情况下,优先选择弱符号解析。
  2. 重定位:链接的文件通常情况下有多个各自的代码和数据节,在重定位的过程中,连接器将多个单独的节合并为一个节,同时在.o的文件相对位置中定位到可执行文件的最终内存位置。
    5.2 在Ubuntu下链接的命令
    使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
    链接命令为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 /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o hello.o

图5.2
如图53用ld命令链接hello.o crt1.o cri.o crtn.o libc.so生成hello文件
5.3 可执,行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

Readelf -h hello 生成文件头信息

图5.3.1
如图5.3Readelf -S hello生成段头表信息

图5.3.2

图5.3.3

使用readelf -s hello 生成符号表信息

图5.3.4

生成了hello可执行文件后每段的地址对应虚拟内存中的虚拟地址。数据段和只读数据段,代码段不可写。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。根据代码节的信息,可以找到代码的起始位置位于0x401090处 ,大小为012f

5.4图
5.5 链接的重定位过程分析
objdump -d -r hello

图5.5
如图5.5,可以发现hello文件的反汇编的结果出现了具体地址,给出了重定位的结果,但是hello.o的反汇编结果中,各部分的开始位置均为0
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
使用edb逐步调试hello将会逐步加载以下函数
在不输入参数的情况下
_dl_start
_dl_init
Hello!_start
__lib_start_main
__libc_csu_init
Hello!_init
Hello!main
Hello!puts@plt
Hello!exit@plt

在输入参数的情况下
_dl_start
_dl_init
Hello!_start
__lib_start_main
__libc_csu_init
Hello!_init
Hello!main
Hello!printf@plt
Hello!atoi@plt
Hello!sleep@plt
Hello!getchar@plt

图5.6
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接器使用工程链接表PLT和全局偏移量表GOT实现函数的动态链接,GOT中存放函数的目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init执行之前,调用的目标地址指向代码,GOT存放的是下一条地址指令,在dl_init执行之后,GOT[1]指向重定位表,根据重定位表,来重新调整调用的函数。
5.8 本章小结
通过链接器将一个可重定位的文件连接上其需要的库函数和外部.c文件的函数之后就称为一个可指向的文件。其中各个文件的代码节和数据节都在重定位之后合并成一个节

第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机科学中最深刻、最成功的概念之一。
当hello程序在计算机中开始执行时,操作系统给了它一种假象,仿佛它是当前系统中唯一正在运行的程序一样,它独自占有一块完整的内存空间,cpu对它指令有求必应,处理器仿佛一直在执行hello这一个程序的指令。
这种状态就称为进程。在进程之中有着一个独立的控制流和一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型的应用级程序,它代表用户运行其他程序。
shell首先打印一个命令行提示符,等待用户输入命令行,然后对命令行进行求值。shell的基本流程是读取命令行,解析命令行,然后代表用户运行程序。
shell首先调用parseline函数,通过这个函数解析以空格分隔的命令行参数,并构造最终会传递给execve的argv向量。
若第一个参数是内置的shell命令名,马上就会解释这个命令。如果不是,shell就会假定这是一个可执行程序,然后在一个新的子进程的上下文中加载并运行这个文件。若最后一个参数是&,那么这个程序将会在后台执行,即shell不会等待其完成。若没有,则这是一个将要在前台执行的程序,shell会显式地等待这个程序执行完成。
6.3 Hello的fork进程创建过程
Hello的进行只通过在终端中输入./hello来实现的
Shell在判断这个参数不是shell的内置命令时,shell会把这条命令作为一个可执行的程序来进行完成。
Shell之后会进行fork函数的执行。Fork函数的作用时创建一个与父进程具有相同的代码、数据、共享库、用户栈和文件描述符的一个子进程。
6.4 Hello的execve过程
在fork一个子进程之后,父进程仍然会执行shell的程序,而子进程会execve加载用户输入的hello程序,由于hello没有加上&符。因此该hello程序会在前台显示的运行

Execve函数具有参数列表argv,参数个数argc和环境变量envp,会读入这写参数变量来作为hello的参数,只有出现错误时execve会返回,其余情况execve只调用不返回。
6.5 Hello的进程执行
上下文是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。用户态和用户内核:执行用户空间的代码的程序,使用用户堆栈。
系统进程:执行内核空间代码(系统调用或中断)的程序,使用内核堆栈(系统堆栈)
当系统调用或中断处理程序返回时,CPU要从内核模式切换回用户模式,此时会执行操作系统的调用程序。如果发现就需队列中有比当前进程更高的优先级的进程,则会发生进程切换:当前进程信息被保存,切换到就绪队列中的那个高优先级进程;否则,直接返回当前进程的用户模式,不会发生上下文切换。

图6.5
6.6 hello的异常与信号处理
Hello可能会出现计时器的中断而产生SIGALRM信号,和shell输入的命令而产生异常。
6.6.1.按下ctrl+c 会发送终止信号

图6.6.1
6.6.2.按下ctrl +z发送SIGST信号使程序挂起

图6.6.2
6.6.3输入ps查看进程
如图6.6.2会发现此时hello的进程在后台进程中而没有终止。
6.6.4输入fg使进程继续进行

图6.6.4
6.6.5输入pstree会显示所有进程之间的关系

图6.6.5
6.6.6乱按键盘,在程序正常执行时乱按键盘,程序依旧能够正常运行

图6.6.6
6.6.7使用kill -KILL命令会杀死PID号的进行

图6.6.7
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章从进程的层次来讨论,从进程的创建、execve的执行,到控制流和异常控制,更加深入了解hello执行层面的操作。

第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。线性地址通常用十六进制数字表示,值的范围从0x00000000到0xffffffff)
虚拟地址:样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。
物理地址:要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。通过直接映射的方式将逻辑地址转化为线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式内存管理单元负责把一个线性地址翻译为物理地址。从管理和效率的角度出发,线性地址被划分成固定长度单位的数组,称为页。多个页合并成为的大数组,我们称之为页表,页表中每一项存储的都是物理页的基地址。同时系统还必须确定这个虚拟页的位置,如果对请求的地址不命中,MMU还将决定需要牺牲的页。
7.4 TLB与四级页表支持下的VA到PA的变换
当一个进程执行一条指令时,发出的地址时虚拟地址并由MMU将虚拟地址转化为物理地址,当MMU得到虚拟地址时,生成PTE,并传给缓存,同时如果缓存能够命中当前PTE那么给MMU返回PTE,MMU构造物理地址,并给缓存,同时缓存将物理地址的数据给CPU。
7.5 三级Cache支持下的物理内存访问
三级cache的思想就是每次访问数据的时候都会将一块较高概率被访问的数据块给予更快的缓存,当多次调用这块地址时,能够更快速的反应找到数据,读取数据。对于不命中的cache,将按级数寻找下一级中的数据,并将该数据写入上一级的cache中。
7.6 hello进程fork时的内存映射
Fork函数被调用时,内核会为其分配内存和PID。其内容和父进程的相同。当映射到相同物理地址上,当子进程想要修改数据时,系统就会采用写时复制的机制,这样就保持了每个进程都独立的虚拟地址。
7.7 hello进程execve时的内存映射
当加载hello程序时,会使用hello的虚拟地址来代替子进程的虚拟空间,然后映射私有区域,为hello的代码和数据创建堆栈的空间,会采用写时复制的机制,然后复制共享区域,与共享库的链接并设置程序计数器。
7.8 缺页故障与缺页中断处理
缺页故障:当MMU无法找到在页表相应的虚拟地址时,此时会发生缺页故障。
缺页中断处理:当缺页异常发生后,缺页处理程序从磁盘加载,然后从下一次页表和主存中读取相应的虚拟地址将其加载到页表中,同时将重新执行取指令的操作。
7.9动态存储分配管理
动态内存管理会管理进程中的堆,其中堆分为对齐的大小不同的块,每个块都被标记为分配的或者空闲的。
其分配器有两种类型:

  1. 显式分配器:将会显式的释放已分配的块。
  2. 隐式分配器:分配器会检测已分配的块是否已经不再使用,然后释放这个块,也被称为垃圾收集器。
    显示分配有两种方法
    1:隐式空闲链表
    块中将会有着头部和脚部的标签,给定一个到头部的隐式空闲链表,唯一的选择将是搜索整个链表,记住前面的位置,直到我们达到当前块。
    其中头部和脚部标签的技术是边界标记技术,这个技术使得我们能够将相邻的空闲块进行合并。
    2:显式空闲链表
    堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个祖先和后继指针。
    在空闲链表指向后一个空闲链表的过程中能够有效缩短寻找块的时间,能够使首次适配的分配时间从块综述的线性时间减少到了空闲块数量的线性时间。
    同时有两种方法来维护链表
    1)使用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用的块。在这种情况下,释放一个块,可以在常数时间内完成。
    2)按照地址顺序来维护链表,其中链表中每个块的地址都小于它祖先的地址。在这种情况下,释放一个块需要线性时间的搜索,来定位合适的祖先。

7.10本章小结
本章通过程序数据取值来表明CPU如何将读数据,MMU对虚拟内存的管理,页表的缺页处理和动态内存分配器的工作原理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
在分时并行多任务系统中,为了合理利用系统设备,达到一定的目标,不允许进程自行决定设备的使用,而是由系统按一定原则统一分配、管理。进程要进行IO操作时,需向操作系统提出IO请求,然后由操作系统根据系统当前的设备使用状况,按照一定的策略,决定对改进程的设备分配。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
unix IO接口:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问的一个 I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
有函数open, read,write,lseek, close可以对接unix IO接口
open函数,函数原型为int open(char * path,int oflag,…)
返回值是一个文件描述符 path顾名思义就是文件名 oflage文件是打开方式 第三个形参应用于创建文件时使用 /创建文件其实还有一个create函数使用 以及openat由于还未使用过这个函数和open的差异 所以不在此处累赘/ open函数 使用if 判断的时候 注意小细节
read函数,函数原型为ssize_t read(int fd, void* buf , size_t nbytes)
返回值是文件读取字节数 在好几种情况下会出现返回值不等于文件读取字节数 也就是第三个参数nbytes的情况 第二个形参buf读取到buf的内存 文件偏移量(current file offset)受改变
write函数,函数原型为ssize_t write(int fd , const void* buf, size_t nbytes)
返回值是文件写入字节数,fd是文件描述符将buf内容写入nbytes个字节到文件 但这里需要注意默认情况是需要在系统队列中等待写入(打开方式不同也会不同) //以上三个出错都返回-1
lseek函数off_t lseek(int fd, off_t offset , int whence)
返回值成功函数返回新的文件偏移量 失败-1 fd文件描述符
off_t是有符号的整数 whence其实是和off_t配套使用的 SEEK_SET文件开始处 SEEK_CUR当前值的相对位置 SEEK_END文件长度±
close函数原型 int close(int fd)
返回值 成功返回0 失败—1 关闭文件描述符
8.3 printf的实现分析
其函数原型为:int printf(const char *format, …);
其函数返回值:打印出的字符格式
其调用格式为:printf("<格式化字符串>", <参量表>);

图8.3.1
其中format控制字符串是字符串,包含了要被写入到标准输出 stdout 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。
接着将会调用vsprintf函数

图8.3.2
在vsprintf函数中将会对format进行格式化,产生格式化输出。
然后printf函数调用write函数
write是一个系统函数,其作用就是从内存buf位置复制最多i个字节到一个文件位置。然后执行syscall指令,使系统产生异常。
在异常处理的过程中,系统会确定需要显示的符号,然后调出vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
#define getchar() getc(stdin)
进入getchar之后,进程会进入阻塞状态,等待外界的输入。系统开始检测键盘的输入。此时如果按下一个键,就会产生一个异步中断。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,read会产生异常,通过系统调用读取按键ascii码,直到接受到回车键才返回。然后将该字符串只保留第一个字符,并作为返回值。
8.5本章小结
IO接口是计算机内部和外部沟通的媒介使用IO接口离不开IO接口的常用函数,然后主要介绍了printf和getchar在IO接口和异常处理过程中的实现。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1.编写源程序
2.预处理:预处理器cpp将hello.c预处理为hello.i文件
3.编译:编译器ccl将hello.i转化成汇编语言的ascii码文件hello.s。
4.汇编:汇编器as将hello.s转化成二进制的机器代码,生成hello.o
5.链接:连接器ld将hello.o和c语言中的库函数连接成
6.运行:shell判断./hello不是内置命令,将创建子进程并加载execve hello
7.访存:通过MMU来根据页表将CPU生成的虚拟地址翻译成物理
8.内存分配:printf使用malloc函数来进行动态内存分配,分配的过程中,将会使用空闲链表的操作。
9.接受信号:通过陷阱异常的方法使得用户能够操纵程序进行和杀死程序,使用ctrl+c 和 ctrl +z来终止和中止程序。
10.IO接口:将计算机内外进行有机结合,使得内部的数据也能显示在电脑屏幕上,同时人们还可以操纵电脑。

一个hello的一生hello.c(Program),经过预处理、编译、汇编、链接,历经艰辛。
在壳(shell)里,进程管理通过fork(Process)来加载(execve),同时进行内存分配(mmap),分配时间片,让hello得以进行取指译码执行和流水线操作等;
存储管理通过MMU连接VA到PA;TLB、4级页表、3级Cache;IO管理与信号处理来完成
就这样一个普通而不简单的hello程序就完成了,通过信号机制可以随意控制hello的运行。使用虚拟内存可以有效解决实际资源不足的问题,使用不同的动态内存管理方式,能够改变各式的内存机制,提高时间效率,在计算机内外连接的IO接口中,真正能够使人与计算机进行互动,多么小的一个程序居然能够有这么伟大的一生,做完了大作业之后,不仅让我对书本的内容加深理解,更让我有着十足的敬佩,发明的计算机让人们受益无穷啊。

附件
Hello.c 源文件
Hello.i 源文件预处理过后的文件
Hello.s 预处理后的文件编译之后的文件
Hello.o 汇编之后的可重定位的文件
Hello.elf hello.o的全部elf信息
Helloexe.elf hello的全部elf信息
Hello 链接生成的可执行的文件
Hello1.s 可执行文件反汇编生成的程序
Hello2.s hello.o反汇编得到的程序

参考文献
[1] https://www.cnblogs.com/chentest/p/5448483.html 关于unix系统接口 普通文件io的小结
[2] https://www.runoob.com/cprogramming/c-function-vsprintf.html C 库函数 - vsprintf()
[3] https://blog.csdn.net/selooloo/article/details/5017335 printf 函数原型
[4] https://blog.csdn.net/prike/article/details/52722934 逻辑地址、线性地址和物理地址的关系
[5] https://blog.csdn.net/yejing_utopia/article/details/41082527 elf和readlef参数的选择
[6] https://www.cnblogs.com/any91/p/7883171.html gcc常用参数详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值