元旦大作业

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 8 -
3.4 本章小结 - 15 -
第4章 汇编 - 16 -
4.1 汇编的概念与作用 - 16 -
4.2 在UBUNTU下汇编的命令 - 16 -
4.3 可重定位目标ELF格式 - 16 -
4.4 HELLO.O的结果解析 - 18 -
4.5 本章小结 - 19 -
第5章 链接 - 20 -
5.1 链接的概念与作用 - 20 -
5.2 在UBUNTU下链接的命令 - 20 -
5.3 可执行目标文件HELLO的格式 - 20 -
5.4 HELLO的虚拟地址空间 - 21 -
5.5 链接的重定位过程分析 - 22 -
5.6 HELLO的执行流程 - 24 -
5.7 HELLO的动态链接分析 - 24 -
5.8 本章小结 - 26 -
第6章 HELLO进程管理 - 27 -
6.1 进程的概念与作用 - 27 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 27 -
6.3 HELLO的FORK进程创建过程 - 27 -
6.4 HELLO的EXECVE过程 - 28 -
6.5 HELLO的进程执行 - 29 -
6.6 HELLO的异常与信号处理 - 30 -
6.7本章小结 - 30 -
第7章 HELLO的存储管理 - 34 -
7.1 HELLO的存储器地址空间 - 34 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 34 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 36 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 38 -
7.5 三级CACHE支持下的物理内存访问 - 39 -
7.6 HELLO进程FORK时的内存映射 - 40 -
7.7 HELLO进程EXECVE时的内存映射 - 40 -
7.8 缺页故障与缺页中断处理 - 41 -
7.9动态存储分配管理 - 42 -
7.10本章小结 - 44 -
第8章 HELLO的IO管理 - 45 -
8.1 LINUX的IO设备管理方法 - 45 -
8.2 简述UNIX IO接口及其函数 - 45 -
8.3 PRINTF的实现分析 - 46 -
8.4 GETCHAR的实现分析 - 48 -
8.5本章小结 - 48 -
结论 - 49 -
附件 - 50 -
参考文献 - 51 -

第1章 概述
1.1 Hello简介
Hello的P2P:hello.c 经过 cpp 的预处理、ccl 的编译、as 的汇编、ld 的链接最终成为可执行目标程序 hello,在shell 中键入启动命令后,shell 为其 fork,产生子进程。Hello.c就从Program变成了Process。
Hello的020: shell 为Process来 execve,进入程序入口后程序开始载入物理内存,然后进入 main 函数执行目标代码,CPU 为运行的 hello 分配时间片执行逻辑控制流。当进程结束后,通过父进程回收,通过内核将数据删除,将Hello运行的所有记录消除。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位;
1.2.3 开发工具
GDB/OBJDUMP;EDB;KDD: vim,gcc,as,ld,edb,readelf,HexEdit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 文件作用
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
hello2.c 测试程序代码
hello2 测试程序
helloo.objdmp Hello.o 的反汇编代码
helloo.elf Hello.o 的 ELF 格式
hello.objdmp Hello 的反汇编代码
hello.elf Hellode ELF 格式
tmp.txt 存放临时数据
1.4 本章小结
本章主要简单介绍了 hello 的p2p,020 过程,列出了本次实验信息:环境、中 间结果。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
程序设计语言的预处理的概念:在编译之前进行的处理。
C语言的预处理主要有三个方面的内容:
1.宏定义;
2.文件包含;
3.条件编译。
预处理命令以符号“#”开头。

预处理阶段:预处理器(cpp)根据字符#开头的命令,修改原始的c程序。得到另一个c程序,通常以.i作为文件扩展名

概念:预处理器 cpp 根据以字符#开头的命令(宏定义、条件编译),修改原 始的 C 程序,将引用的所有库展开合并成为一个完整的文本文件。
主要功能如下:
1、 将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c 第 6-8 行中的#include 等命令告诉预处理器读取系统头文件 stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。
2、 用实际值替换用#define 定义的字符串
3、 根据#if 后面的条件决定需要编译的代码
2.2在Ubuntu下预处理的命令

图2.1 使用 gcc 命令生成 hello.i 文件

2.3 Hello的预处理结果解析

使用 GVim 打开 hello.i,找到main 函数出现在 hello.c 中的代码自 3099 行开始。如下:

图2.2 hello.i 中 main 函数的位置

以 stdio.h 的展开为例,
cpp 到默认的环境变量下寻找 stdio.h,打开/usr/include/stdio.h 发现其中依然使用了#define 语句,cpp 对此递归展开,所以最终.i 程序中是没有#define 的。而且发现其中使用了大量的#ifdef #ifndef 的语句,cpp 会对条件值进行判断来决定是否执行
2.4 本章小结
本章主要介绍了预处理的定义与作用、并结合预处理之后的程序对预处理结果进行了解析。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序

编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。
这个过程称为编译,同时也是编译的作用。
编译器的构建流程主要分为 3 个步骤:

  1. 词法分析器,用于将字符串转化成内部的表示结构。
  2. 语法分析器,将词法分析得到的标记流(token)生成一棵语法树。
  3. 目标代码的生成,将语法树转化成目标代码
    3.2 在Ubuntu下编译的命令

图3.1 使用 gcc 命令生成 64 位的 hello.s 文件
3.3 Hello的编译结果解析

3.3.0 汇编指令
指令 含义
.file 声明源文件
.text 以下是代码段
.section .rodata 以下是 rodata 节
.globl 声明一个全局变量
.type 用来指定是函数类型或是对象类型
.size 声明大小
.long、.string 声明一个 long、string 类型
.align 声明对指令或者数据的存放地址进行对齐的方式
3.3.1 数据
hello.s 中用到的 C 数据类型有:整数、字符串、数组。
一、 字符串
程序中的字符串分别是:
1) “Usage: Hello 学号 姓名!\n”,第一个 printf 传入的输出格式化参数,
在 hello.s 中声明如图 3.2,可以发现字符串被编码成 UTF-8 格式,一个
汉字在 utf-8 编码中占三个字节,一个\代表一个字节。
2) “Hello %s %s\n”,第二个 printf 传入的输出格式化参数,在 hello.s 中
声明如图 3.2。
其中后两个字符串都声明在了.rodata 只读数据节。

图3.2 hello.s 中声明在.LC0 和.LC1 段中的字符串

二、 整数
程序中涉及的整数有:
1) int sleepsecs:sleepsecs 在 C 程序中被声明为全局变量,且已经被 赋值,编译器处理时在.data 节声明该变量,.data 节存放已经初始化的 全局和静态 C 变量。在图 3.3 中,可以看到,编译器首先将 sleepsecs 在.text 代码段中声明为全局变量,其次在.data 段中,设置对齐方式为 4、设置类型为对象、设置大小为 4 字节、设置为 long 类型其值为 2 (long 类型在 linux 下与 int 相同为 4B,将 int 声明为 long 应该是编译 器偏好)。

图 3.3 hello.s 中 sleepsecs 的声明
2) int i:编译器将局部变量存储在寄存器或者栈空间中,在 hello.s 中 编译器将 i 存储在栈上空间-4(%rbp)中,可以看出 i 占据了栈中的 4B。
3) int argc:作为第一个参数传入。
4) 立即数:其他整形数据的出现都是以立即数的形式出现的,直接 硬编码在汇编代码中。

三、 数组
程序中涉及数组的是:char argv[] main,函数执行时输入的命令行, argv 作为存放 char 指针的数组同时是第二个参数传入。
argv 单个元素 char
大小为 8B,argv 指针指向已经分配好的、一片存 放着字符指针的连续空间,起始地址为 argv,main 函数中访问数组元素 argv[1],argv[2]时,按照起始地址 argv 大小 8B 计算数据地址取数据,在 hello.s 中,使用两次(%rax)(两次 rax 分别为 argv[1]和 argv[2]的地址)取 出其值。如图 3.4。

图 3.4 计算地址取出数组值
.L4:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx argv[1]地址
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax argv[2]地址
movq %rax, %rsi
leaq .LC1(%rip), %rdi
3.3.2 赋值
程序中涉及的赋值操作有:
1) int sleepsecs=2.5 :因为 sleepsecs 是全局变量,所以直接在.data 节中 将 sleepsecs 声明为值 2 的 long 类型数据。
2) i=0:整型数据的赋值使用 mov 指令完成,根据数据的大小不同使用不 同后缀,分别为:
指令 b w l q
大小 8b (1B) 16b (2B) 32b (4B) 64b (8B)
因为 i 是 4B 的 int 类型,所以使用 movl 进行赋值,汇编代码如图 3.5。

图3.5 hello.s 中变量 i 的赋值
3.3.3 类型转换
程序中涉及隐式类型转换的是:int sleepsecs=2.5,将浮点数类型的 2.5 转换为 int 类型。
当在 double 或 float 向 int 进行类型转换的时候,程序改变数值和位模式的原则是:值会向零舍入。遵从向零舍入的原则,将 2.5 舍入为 2。
3.3.4 算数操作
进行数据算数操作的汇编指令有:
指令 效果
leaq S,D D=&S
INC D D+=1
DEC D D-=1
NEG D D=-D
ADD S,D D=D+S
SUB S,D D=D-S
IMULQ S R[%rdx]:R[%rax]=SR[%rax](有符号)
MULQ S R[%rdx]:R[%rax]=S
R[%rax](无符号)
IDIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(有符号)
R[%rax]=R[%rdx]:R[%rax] div S
DIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(无符号) R[%rax]=R[%rdx]:R[%rax] div S
程序中涉及的算数操作有:
1) i++,对计数器 i 自增,使用程序指令 addl,后缀 l 代表操作数是一个 4B 大小的数据。
2) 汇编中使用 leaq .LC1(%rip),%rdi,使用了加载有效地址指令 leaq 计算 LC1 的段地址%rip+.LC1 并传递给%rdi。
3.3.5 关系操作
进行关系操作的汇编指令有:
指令 效果 描述
CMP S1,S2 S2-S1 比较-设置条件码
TEST S1,S2 S1&S2 测试-设置条件码
SET** D D=** 按照将条件码设置D
J
—— 根据**与条件码进行跳转
程序中涉及的关系运算为:
1) argc!=3:判断 argc 不等于 3。hello.s 中使用 cmpl $3,-20(%rbp),计算 argc-3 然后设置条件码,为下一步 je 利用条件码进行跳转作准备。
2) i<10:判断i小于10。hello.s中使用cmpl $9,-4(%rbp),计算i-9然后设置条件码,为下一步jle利用条件码进行跳转做准备。
3.3.6 控制转移
程序中涉及的控制转移有:
1) if (argv!=3):当 argv 不等于 3 的时候执行程序段中的代码。如图 3.6,对 于 if 判断,编译器使用跳转指令实现,首先 cmpl 比较 argv 和 3,设置条件 码,使用 je 判断 ZF 标志位,如果为 0,说明 argv-3=0 argv==3,则不执行 if 中的代码直接跳转到.L2,否则顺序执行下一条语句,即执行 if 中的代码。

图3.6 if 语句的编译
je .L2 相等直接跳转
2) for(i=0;i<10;i++):使用计数变量 i 循环 10 次。如图 3.7,编译器的编译逻辑是,首先无条件跳转到位于循环体.L4 之后的比较代码,使用 cmpl 进行 比较,如果 i<=9,则跳入.L4 for 循环体执行,否则说明循环结束,顺序执 行 for 之后的逻辑。

图3.7 for 循环的编译
.L2:
movl $0, -4(%rbp)
jmp .L3 无条件跳转
.L4:
movq -32(%rbp), %rax
.L3:
cmpl $9, -4(%rbp)
jle .L4 跳入L4循环体执行

3.3.7 函数操作
函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和可选 的返回值实现某种功能。P 中调用函数 Q 包含以下动作:
1) 传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始 地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的 地址。
2) 传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回 一个值。
3) 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回 前,又必须释放这些空间。
64 位程序参数存储顺序(浮点数使用 xmm,不包含):
1 2 3 4 5 6 7
%rdi %rsi %rdx %rcx %r8 %r9 栈空间
程序中涉及函数操作的有:
1) main 函数:
a) 传递控制,main 函数因为被调用 call 才能执行(被系统启动函数 __libc_start_main 调用),call 指令将下一条指令的地址 dest 压栈, 然后跳转到 main 函数。
b) 传递数据,外部调用过程向 main 函数传递参数 argc 和 argv,分别 使用%rdi 和%rsi 存储,函数正常出口为 return 0,将%eax 设置 0 返回。 c) 分配和释放内存,使用%rbp 记录栈帧的底,函数分配栈帧空间 在%rbp 之上,程序结束时,调用 leave 指令,leave 相当于 mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后 ret 返回,ret 相当 pop IP,将下一条要执行指令的地址设置为 dest。
2) printf 函数:
a) 传递数据:第一次 printf 将%rdi 设置为“Usage: Hello 学号 姓名! \n”字符串的首地址。第二次 printf 设置%rdi 为“Hello %s %s\n” 的首地址,设置%rsi 为 argv[1],%rdx 为 argv[2]。
b) 控制传递:第一次 printf 因为只有一个字符串参数,所以 call puts@PLT;第二次 printf 使用 call printf@PLT。
3) exit 函数:
a) 传递数据:将%edi 设置为 1。
b) 控制传递:call exit@PLT。
4) sleep 函数:
a) 传递数据:将%edi 设置为 sleepsecs。
b) 控制传递:call sleep@PLT。
5) getchar 函数:
a) 控制传递:call gethcar@PLT
3.4 本章小结
编译器将预处理后的文本进行汇编处理,对于常量,编译器将它储存到一个特定的位置,记录它的一些信息,比如类型;对于一些特定的常量,比如printf()函数中的信息,编译器会把它提取出来保存。程序中的语句,例如赋值语句,编译器通过寄存器,栈等结构进行赋值;分支语句用je,jle,jge等条件跳转语句进行实现。每种语句都有对应的实现方法。程序中的函数,如果不是库函数,则会对函数进行逐句的语法分析和解析,如果是标准的库函数,编译器可以直接用call语句进行调用。
汇编语言相对于高级语言来说,它更加靠近底层机器且直接面对硬件的,所以也为高级语言提供了一种统一的面向机器的解释,它具有一些助记符,所以比直接的机器语言好理解,但相对于高级语言又显得难以掌握。汇编语言具有:机器相关性,高速度和高效率,编写和调试的复杂性等特性。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位 目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它 包含程序的指令编码。这个过程称为汇编,亦即汇编的作用。
4.2 在Ubuntu下汇编的命令

图4.1 使用 gcc 指令生成 hello.o 文件
4.3 可重定位目标elf格式
使用 readelf -a hello.o > helloo.elf 指令获得 hello.o 文件的 ELF 格式。其组成如下:
1) ELF Header:以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统 的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。

图 4.2 ELF Header

2) Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。

图4.3 节头部表 Section Headers
3) 重定位节.rela.text ,一个.text 节中位置的列表,包含.text 节中需要进行重定 位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位 置。如图 4.4,图中 8 条重定位信息分别是对.L0(第一个 printf 中的字符 串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf 函数、 sleepsecs、sleep 函数、getchar 函数进行重定位声明。

图4.4 重定位节.rela.text

.rela 节的包含的信息有(readelf 显示与 hello.o 中的编码不同,以 hello.o 为准):
offset 需要进行重定向的代码在.text 或.data 节中的偏移位置,8 个 字节
Info 包括 symbol 和 type 两部分, 其中 symbol 占前 4 个字节, type 占后 4 个字节,symbol 代 表重定位到的目标在.symtab 中的偏移量,type 代表重定位 的类型
Addend 计算重定位位置的辅助信息, 共占 8 个字节
Type 重定位到的目标的类型
Name 重定向到的目标的名称

下面以.L1 的重定位为例阐述之后的重定位过程:链接器根据 info 信息 向.symtab 节中查询链接目标的符号,由 info.symbol=0x05,可以发现重定位目标 链接到.rodata 的.L1,设重定位条目为 r,根据图 4.5 知 r 的构造为:
r.offset=0x18, r.symbol=.rodata, r.type=R_X86_64_PC32, r.addend=-4,
重定位一个使用 32 位 PC 相对地址的引用。计算重定位目标地址的算法如 下(设需要重定位的.text 节中的位置为 src,设重定位的目的位置 dst):
refptr = s +r.offset (1)
refaddr = ADDR(s) + r.offset (2)
refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)(3)
其中(1)指向 src 的指针(2)计算 src 的运行时地址,(3)中, ADDR(r.symbol)计算 dst 的运行时地址,在本例中,ADDR(r.symbol)获得 的是 dst 的运行时地址,因为需要设置的是绝对地址,即 dst 与下一条指令 之间的地址之差,所以需要加上 r.addend=-4。 之后将 src 处设置为运行时值
refptr,完成该处重定位。

图4.5 通过 HexEdit 查看 hello.o 中的.rela.text 节

3).rela.eh_frame : eh_frame 节的重定位信息。
4).symtab:符号表,用来存放程序中定义和引用的函数和全局变量的信息。 重定位需要引用的符号都在其中声明。

4.4 Hello.o的结果解析
使用 objdump -d -r hello.o > helloo.objdump 获得反汇编代码。
总体观察后发现,除去显示格式之外两者差别不大,主要差别如下:
1) 分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段 名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显 然不存在,而是确定的地址。
2) 函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call 的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执 行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用, 将其 call 指令后的相对地址设置为全 0(目标地址正是下一条指令),然 后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
3) 全局变量访问:在.s 文件中,访问 rodata(printf 中的字符串),使用段名 计算机系统课程报告 - 20 - 称+%rip,在反汇编代码中 0+%rip,因为 rodata 中数据地址也是在运行时 确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置 为全 0 并添加重定位条目。

4.5 本章小结
汇编器对hello.s文件进行汇编,生成了可重定位文件。对文件的全局变量,函数,程序语句都进行了分析,给出了初始的相对位置信息,相当于对整个文件做出了一个初始的整理,为后面的链接等操作做准备。
(第4章1分)

第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.1 使用 ld 命令链接生成可执行程序 hello
注意:因为需要生成的是 64 位的程序,所以,使用的动态链接器和链接的目 标文件都应该是 64 位的。
5.3 可执行目标文件hello的格式
使用 readelf -a hello > hello.elf 命令生成 hello 程序的 ELF 格式文件。
在 ELF 格式文件中,Section Headers 对 hello 中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信 息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。

图 5.2 hello ELF 格式中的 Section Headers Table
5.4 hello的虚拟地址空间
在edb的Data Dump一栏中可以看到程序的虚拟地址。
如图 5.3,在hello的ELF文件的“程序头”一节中,也可以找到各节对应的虚拟地址:
在下面可以看出,程序包含 8 个段:
1) PHDR 保存程序头表。
2) INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释 器(如动态链接器)。
3) LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了 常量数据(如字符串)、程序的目标代码等。
4) DYNAMIC 保存了由动态链接器使用的信息。
5) NOTE 保存辅助信息。
6) GNU_STACK:权限标志,标志栈是否是可执行的。
7) GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。

图5.3 ELF 格式文件中的 Program Headers Table

5.5 链接的重定位过程分析
使用 objdump -d -r hello > hello.objdump 获得 hello 的反汇编代码。
与 hello.o 反汇编文本 helloo.objdump 相比,在 hello.objdump 中多了许多节, 列在下面。
节名称 描述
.interp 保存 ld.so 的路径
.note.ABI-tag Linux 下特有的 section
.hash 符号的哈希表
.gnu.hash GNU 拓展的符号的哈希表
.dynsym 运行时/动态符号表
.dynstr 存放.dynsym 节中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 运行时/动态重定位表
.rela.plt .plt 节的重定位条目
.init 程序初始化需要执行的代码
.plt 动态链接-过程链接表
.fini 当程序正常终止时需要执行的代码
.eh_frame contains exception unwinding and source language information.
.dynamic 存放被 ld.so 使用的动态链接信息
.got 动态链接-全局偏移量表-存放变量
.got.plt 动态链接-全局偏移量表-存放函数
.data 初始化了的数据
.comment 一串包含编译器的 NULL-terminated 字符串
分析:可以明显看出,可执行文件对每个函数给出了重定位处理,给出了它们的绝对物理地址;而在hello.o中,只是给出了每个函数对于程序首地址的偏移地址。

通过上面两个文件,可以了解到链接实现的过程:将可重定位文件中的.text节中函数以及全局变量的相对地址转变为了绝对地址,全局变量的寻址方式0x0%rsp也将0x0改为了确定的地址。

对hello.中的重定位节中的定位:在hello.o中,给出了函数和全局变量相对于EIF头的偏移量,所以在链接后,给定了程序首地址,然后根据偏移量,计算出函数和全局变量的绝对地址。
5.6 hello的执行流程
使用 edb 执行 hello,观察函数执行流程,将过程中执行的主要函数列在下面:
程序名称 程序地址
ld-2.27.so!_dl_start 0x7fce 8cc38ea0
ld-2.27.so!_dl_init 0x7fce 8cc47630
hello!_start 0x400500
libc-2.27.so!__libc_start_main 0x7fce 8c867ab0
-libc-2.27.so!__cxa_atexit 0x7fce 8c889430
-libc-2.27.so!__libc_csu_init 0x4005c0
hello!_init 0x400488
libc-2.27.so!_setjmp 0x7fce 8c884c10
-libc-2.27.so!_sigsetjmp 0x7fce 8c884b70
–libc-2.27.so!__sigjmp_save 0x7fce 8c884bd0
hello!main 0x400532
hello!puts@plt 0x4004b0
hello!exit@plt 0x4004e0
*hello!printf@plt –
*hello!sleep@plt –
*hello!getchar@plt –
ld-2.27.so!_dl_runtime_resolve_xsave 0x7fce 8cc4e680
-ld-2.27.so!_dl_fixup 0x7fce 8cc46df0
–ld-2.27.so!_dl_lookup_symbol_x 0x7fce 8cc420b0
libc-2.27.so!exit 0x7fce 8c889128

5.7 Hello的动态链接分析
分析:对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,在链接时,对所有绝对地址的引用不做重定位,把这一步推迟到装载的时候进行;一旦模块装载地址确定,即目标地址确定,则系统对程序中所有的绝对地址的引用进行重定位。
在 dl_init 调用之前,对于每一条 PIC 函数调用,调用的目标地址都实际指向 PLT 中的代码逻辑,GOT 存放的是 PLT 中函数调用指令的下一条指令地址。如在 图 5.4 (a)。
在 dl_init 调用之后,如图 5.4 (b),0x601008 和 0x601010 处的两个 8B 数据分 别发生改变为 0x7fd9 d3925170 和 0x7fd9 d3713680,如图 5.4(c)其中 GOT[1]指 向重定位表(依次为.plt 节需要重定位的函数的运行时地址)用来确定调用的函数 地址,如图 5.4(d)GOT[2]指向动态链接器 ld-linux.so 运行时地址。

图 5.4 (a) 没有调用 dl_init 之前的全局偏移量表.got.plt
(根据.plt 中 exit@plt jmp 的引用地址 0x601030 可以得到其.got.plt 条目为 0x4004e6,正是其下条指令地址)

图5.4(b)调用 dl_init 之后的全局偏移量表.got.plt

图5.3(c)0x7fd9 d3925170 指向的重定位表

图 5.4(d) 0x7fd9 d3713680 目标程序-动态链接器
在之后的函数调用时,首先跳转到 PLT 执行.plt 中逻辑,第一次访问跳转时 GOT 地址为下一条指令,将函数序号压栈,然后跳转到 PLT[0],在 PLT[0]中将重 定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位 表确定函数运行时地址,重写 GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。 因为在 PLT 中使用的 jmp,所以执行完目标函数之后的返回地址为最近 call 指 令下一条指令地址,即在 main 中的调用完成地址。
5.8 本章小结
链接在程序编译的过程中有着十分重要的作用,它为程序处理好了需要的绝大多数资源,将所需要的函数,变量等信息都整理好,给出了绝对地址,使得程序完整,可以执行。
而执行链接的链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是操作系统一个正在运行的程序的一种抽象。程序在系统上运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器,主存和I/O设备。处理器看上去就像在不间断地一条接一条的执行程序中的指令。

进程的作用:通过进程的概念,系统可以实现多线程,并发运行等操作。
6.2 简述壳Shell-bash的作用与处理流程
Shell 的作用:Shell 是一个用 C 语言编写的程序,他是用户使用 Linux 的桥梁。 Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问 操作系统内核的服务。
处理流程:
分析命令行中的参数,分辨是内置命令还是可执行文件,是内置命令则执行相应操作,如果是可执行文件,则fork()产生子进程,execve()加载程序,在子进程中完成请求的操作,然后结束进程,并回收。
6.3 Hello的fork进程创建过程
在终端 Gnome-Terminal 中键入 ./hello 学号 姓名,运行的终端程序会 对输入的命令行进行解析,因为 hello 不是一个内置的 shell 命令所以解析之后终端 程序判断./hello 的语义为执行当前目录下的可执行目标文件 hello,之后终端程序 首先会调用 fork 函数创建一个新的运行的子进程,新创建的子进程几乎但不完全 与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的) 一份副本,这就意味着,当父进程调用 fork 时,子进程可以读写父进程中打开的 任何文件。父进程与子进程之间最大的区别在于它们拥有不同的 PID。
父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.4 Hello的execve过程
当 fork 之后,子进程调用 execve 函数(传入命令行参数)在当前进程的上下 文中加载并运行一个新程序即 hello 程序,execve 调用驻留在内存中的被称为启动 加载器的操作系统代码来执行 hello 程序,加载器删除子进程现有的虚拟内存段, 并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚 拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化 为可执行文件中的内容。最后加载器设置 PC 指向_start 地址,_start 最终调用 hello 中的 main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据 复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用 它的页面调度机制自动将页面从磁盘传送到内存。
加载器创建的内存映像如下:

图 6.2 启动加载器创建的系统映像
6.5 Hello的进程执行
当创建子进程后,子进程就有了它自己的上下文,包括存放在内存的程序的代码和数据,它的栈,通用目的寄存器中的内容等。进程在处理器中运行,处理器中有多个进程,但是它通过逻辑控制流提供一个假象,好像每个进程独占处理器,关键在于进程是轮流使用处理器,在不同的时间片上使用。此外处理器还通过某个控制寄存器中的一个模式位提供内核模式,当设置模式位时,就是内核模式,可以享有特权,没有设置模式位时,就是用户模式,没有特权。通过改变模式位来切换两种模式。
6.6 hello的异常与信号处理
如图 6.4(a),是正常执行 hello 程序的结果,当程序执行完成之后,进程被 回收。
如图 6.4(b),是在程序输出 2 条 info 之后按下 ctrl-z 的结果,当按下 ctrl-z 之后,shell 父进程收到 SIGSTP 信号,信号处理函数的逻辑是打印屏幕回显、将 hello 进程挂起,通过 ps 命令我们可以看出 hello 进程没有被回收,此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。
如图 6.4(c)是在程序输出 3 条 info 之后按下 ctrl-c 的结果,当按下 ctrl-c 之 后,shell 父进程收到 SIGINT 信号,信号处理函数的逻辑是结束 hello,并回收 hello 进程。
如图 6.4(d)是在程序运行中途乱按的结果,可以发现,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其 他字串会当做 shell 命令行输入。

图6.4 (a) 正常运行 hello 程序

图6.4(b)运行中途按下 ctrl-z

图6.4(c)运行中途按下 ctrl-c

图6.4(d)运行中途乱按
6.7本章小结
hello的可执行文件通过shell来接受,分析,然后通过fork()为hello开辟子程序,然后execve()函数将程序加载进去,这时候hello就从程序变成了进程,fork()函数给于了hello进程和父进程一样的资源,所以hello进程具有了上下文。
处理器对hello进程提供逻辑控制流的抽象,控制hello进程的运行时间片,并且通过模式位来定义内核模式和用户模式,让hello进程可以进行上下文切换。
hello进程在运行过程中需要处理异常,对异常作出反应。
hello进程终止后,将被父进程回收,然后删除它的上下文,这就是整个hello进程开始到结束。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:CPU 通过地址总线的寻址,找到真实的物理内存对应地址。 CPU 对内存的访问是通过连接着 CPU 和北桥芯片的前端总线来完成的。在前端总线上 传输的内存地址都是物理内存地址。
逻辑地址:程序代码经过编译后出现在 汇编程序中地址。逻辑地址由选择符 (在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏 移部分)组成。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合 形式。分页机制中线性地址作为输入。
至于虚拟地址,只关注 CSAPP 课本中提到的虚拟地址,实际上就是这里的线性地址。

图 7.1[转] 三种地址之间的关系
7.2 Intel逻辑地址到线性地址的变换-段式管理
最初 8086 处理器的寄存器是 16 位的,为了能够访问更多的地址空间但不改 变寄存器和指令的位宽,所以引入段寄存器,8086 共设计了 20 位宽的地址总线, 通过将段寄存器左移 4 位加上偏移地址得到 20 位地址,这个地址就是逻辑地址。 将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个 数据寄存器。
分段功能在实模式和保护模式下有所不同。
实模式,即不设防,也就是说逻辑地址=线性地址=实际的物理地址。段寄存 器存放真实段基址,同时给出 32 位地址偏移量,则可以访问真实物理内存。
在保护模式下,线性地址还需要经过分页机制才能够得到物理地址,线性地 址也需要逻辑地址通过段机制来得到。段寄存器无法放下 32 位段基址,所以它们 被称作选择符,用于引用段描述符表中的表项来获得描述符。描述符表中的一个 条目描述一个段,构造如下:

图 7.2[转] 段描述符表中的一个条目的构造
Base:基地址,32 位线性地址指向段的开始。Limit:段界限,段的大小。 DPL: 描述符的特权级 0(内核模式)-3(用户模式)。
所有的段描述符被保存在两个表中:全局描述符表GDT和局部描述符表LDT。 gdtr 寄存器指向 GDT 表基址。
段选择符构造如下:

图 7.3[转] 段选择符的构造
TI:0 为 GDT,1 为 LDT。Index 指出选择描述符表中的哪个条目,RPL 请求 特权级。
所以在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符 在段描述符表中根据 Index 选择目标描述符条目 Segment Descriptor,从目标描述 符中提取出目标段的基地址 Base address,最后加上偏移量 offset 共同构成线性地 址 Linear Address。保护模式时分段机制图示如下:

图 7.4[转] 保护模式下分段机制
当 CPU 位于 32 位模式时,内存 4GB,寄存器和指令都可以寻址整个线性地址 空间,所以这时候不再需要使用基地址,将基地址设置为 0,此时逻辑地址=描述 符=线性地址,Intel 的文档中将其称为扁平模型(flat model),现代的 x86 系统内 核使用的是基本扁平模型,等价于转换地址时关闭了分段功能。在 CPU 64 位模式 中强制使用扁平的线性空间。逻辑地址与线性地址就合二为一了。所以分段机制 也就成为时代的眼泪了(?
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的变换是通过分页机制完成的。
首先先分析线性地址的组成:设虚拟地址长度为n,它由虚拟页号(VPN)和虚拟页偏移(VPO)组成,根据虚拟页面大小,可以确定VPO所占的字节长度;而虚拟页号(VPN)又由TLB索引(TLBT)和TLB标记(TLBI)组成,可以根据TLB的大小,确定它们所占的字节。

通过虚拟地址得到虚拟页号(VPN),然后将虚拟页号(VPN)传入TLB中得到对应的物理页号,然后结合虚拟偏移(VPO),确定唯一的物理地址;若是在TBL中找不到,则需要从高速缓存或者磁盘中取出对应的虚拟页表,放入TLB再进行操做。
1)TLB命中

2)缺页

7.4 TLB与四级页表支持下的VA到PA的变换
若TLB命中,则从TLB中可以直接找到各级页表,然后得到PPN,与PPO结合即可得到物理地址。若TLB不命中,则需要从高速缓存中到PPN。
7.5 三级Cache支持下的物理内存访问
先对物理地址分析:由CT,CI,CO三部分组成。CT为缓存标记位;CI为缓存组索引;CO为缓存偏移。
在上一步中我们已经获得了物理地址VA,使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为子程序复制了一份和父进程完全一样但独立的用户级虚拟地址空间,包括代码和数据段,堆,共享库,以及用户栈。并且创建了当前进程的mm_struct,区域结构和页表的原样副本,内核将两个进程中的每个页面都标记为只读,并且将两个进程的区域结构都标记为私有的写时复制。当有进程想要修改私有区域的某个页面时,就会触发写时复制。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:
1) 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
2) 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3) 映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4) 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。

图7.9 加载器是如何映射用户地址空间区域的
7.8 缺页故障与缺页中断处理
DRAM缓存不命中成为缺页。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。接下来,内核从磁盘复制VP3到内存的PP3,更新PTE3,随后返回。
7.9动态存储分配管理
printf 函数会调用 malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用 来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已 分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分 配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块, 自动释放未使用的已经分配的块的过程叫做垃圾收集。
一、 带边界标签的隐式空闲链表
1)堆及堆中内存块的组织结构:

在内存块中增加 4B 的 Header 和 4B 的 Footer,其中 Header 用于寻找下一 个 blcok,Footer 用于寻找上一个 block。Footer 的设计是专门为了合并空闲块方便的。因为 Header 和 Footer 大小已知,所以我们利用 Header 和 Footer 中存放的块大小就可以寻找上下 block。
2)隐式链表
所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链 接,而是将对内存空间中的所有块组织成一个大链表,其中 Header 和 Footer 中的 block 大小间接起到了前驱、后继指针的作用。
3)空闲块合并 因为有了 Footer,所以我们可以方便的对前面的空闲块进行合并。合并的 情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于 四种情况分别进行空闲块合并,我们只需要通过改变 Header 和 Footer 中的值 就可以完成这一操作。
二、 显示空间链表基本原理 将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表, 在每个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针,如下图:

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线 性时间减少到了空闲块数量的线性时间。
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始 处,使用 LIFO 的顺序和首次适配的放置策略,分配器会最先检查最近使用过 的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界 标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链 表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要 线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
hello刚开始给出的逻辑地址,在汇编的时候由汇编器整理得到,然后hello变为进程的时候,就要把逻辑地址变为虚拟地址,当需要操作的时候,就将虚拟地址翻译成物理地址。
虚拟地址的储存在磁盘中,为了优化,在内存中建立虚拟地址缓冲DRAM,并利用页表结构来储存虚拟地址,每个页表由页表项组成,将虚拟页从磁盘中取出,然后放入DRAM的物理页中;为了简化页表,还使用了多级页表结构。将虚拟地址翻译为物理地址后,物理地址在cache和主存中访问数据。
hello中需要用到的动态存储空间堆,堆中的空间有的是以分配,有的是未分配,为了更好的运行程序,利用资源,堆中的空闲块采用隐式和显式链表来储存,而显式空闲链表又可以采用分离链表来储存。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的 IO 设备都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
Unix I/O 接口统一操作:
1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
2) Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。
3) 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
4) 读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
5) 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。

Unix I/O 函数:
1) int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。open 函数将 filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
2) int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
3) ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4) ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
8.3 printf的实现分析
前提:printf 和 vsprintf 代码是 windows 下。
查看 printf 代码:

首先 arg 获得第二个不定长参数,即输出的时候格式化串对应的值。 查看 vsprintf 代码:

则知道 vsprintf 程序按照格式 fmt 结合参数 args 生成格式化之后的字符串,并 返回字串的长度。
在 printf 中调用系统函数 write(buf,i)将长度为i的 buf输出。write 函数如下:

在 write 函数中,将栈中参数放入寄存器,ecx 是字符个数,ebx 存放第一个 字符地址,int INT_VECTOR_SYS_CALLA 代表通过系统调用 syscall,查看 syscall 的实现:

syscall 将字符串中的字节“Hello 1170300114 wangbingjie”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的 ASCII 码。
字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存 储到 vram 中。
显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器 传输每一个点(RGB 分量)。
于是我们的打印字符串“Hello 1170300114 wangbingjie”就显示在了屏幕上
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
对于 printf 和 getchar,hello 以前只知道调用之后一个能打印字符,一个能读入字符,可 究竟为啥,不知道,学习完 Linux 都市的 IO 管理手册之后,hello 多少明白了其中奥妙,原来他 们都是 Unix I/O 的封装,而真正调用的是 write 和 read 这样的系统调用函数,而它们又都是由 内核完成的,之所以键盘能输入是因为引发了异步异常,之所以屏幕上会有显示是因为字符串被 复制到了屏幕赖以显示的显存当中,至于其中细节,也值得好好研究一番……
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
(第8章1分)
结论
hello 程序 终于 完成了它 艰辛 的一生。
hello 的一生大事记如下:
1) 编写,通过 editor 将代码键入 hello.c
2) 预处理,将 hello.c 调用的所有外部的库展开合并到一个 hello.i 文件中 3) 编译,将 hello.i 编译成为汇编文件 hello.s
4) 汇编,将 hello.s 会变成为可重定位目标文件 hello.o
5) 链接,将 hello.o 与可重定位目标文件和动态链接库链接成为可执行目 标程序 hello
6) 运行:在 shell 中输入./hello 1170300114 wangbingjie
7) 创建子进程:shell 进程调用 fork 为其创建子进程
8) 运行程序:shell 调用 execve,execve 调用启动加载器,加映射虚拟内 存,进入程序入口后程序开始载入物理内存,然后进入 main 函数。
9) 执行指令:CPU 为其分配时间片,在一个时间片中,hello 享有 CPU 资源,顺序执行自己的控制逻辑流
10) 访问内存:MMU 将程序中使用的虚拟内存地址通过页表映射成物 理地址。
11) 动态申请内存:printf 会调用 malloc 向动态内存分配器申请堆中的 内存。
12) 信号:如果运行途中键入 ctr-c ctr-z 则调用 shell 的信号处理函数分 别停止、挂起。
13) 结束:shell 父进程回收子进程,内核删除为这个进程创建的所有 数据结构。

(结论0分,缺失 -1分,根据内容酌情加分)

附件
文件名称 文件作用
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
hello2.c 测试程序代码
hello2 测试程序
helloo.objdmp Hello.o 的反汇编代码
helloo.elf Hello.o 的 ELF 格式
hello.objdmp Hello 的反汇编代码
hello.elf Hellode ELF 格式
tmp.txt 存放临时数据

(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] ELF 构造:https://www.cs.stevens.edu/~jschauma/631/elf.html
[1] 16 进制计算器:http://www.99cankao.com/digital-computation/hex-calculator.php
[2] Linux 下进程的睡眠唤醒:https://blog.csdn.net/shengin/article/details/21530337
[3]进程的睡眠、挂起和阻塞:https://www.zhihu.com/question/42962803
[4] 虚拟地址 、 逻辑地址 、线性地址 、物理地址 : https://blog.csdn.net/rabbit_in_android/article/details/49976101
[5] printf 函数实现的深入 剖 析 : https://blog.csdn.net/zhengqijun_/article/details/72454714
[6] 内存地址转换与分段 https://blog.csdn.net/drshenlei/article/details/4261909
(参考文献0分,缺失 -1分)

猜你喜欢

转载自blog.csdn.net/aurora0_0/article/details/85492082