计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 自动化类(人工智能辅修)
学   号 1180400404
班   级 1804004
学 生 王梓硕    
指 导 教 师 史先俊

计算机科学与技术学院
2020年3月
摘 要
本文通过论述“hello”从hello.c“出生”到生成hello可执行文件,再到结束进程,“hello”“死亡”的过程,生动形象有条理地对预处理、编译、汇编、链接的过程进行讨论,并对hello的进程管理、存储管理和IO管理中的内容进行分析,最终理解hello“P2P”: From Program to Process、“O2O”: From Zero-0 to Zero-0的整体过程

关键词:CS;OS;IO管理;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 -

第1章 概述
1.1 Hello简介
P2P:From Program to Process。hello.c经过cpp的预处理产生hello.i、cll的编译翻译为汇编程序hello.s、as的汇编将hello.s翻译成机器语言指令、将指令打包成可重定位的目标文件hello.o,ld的链接获得可执行目标文件hello,之后再在shell中键入命令启动后,shell为其fork产生子进程的过程,即为P2P;
O2O:From Zero-0 to Zero -0,Shell通过execve在fork产生的子进程中加载hello,映射虚拟内存,先删除当前虚拟地址的数据结构并为hello创建新的区域结构,进入程序入口后程序开始载入物理内存。随后进入main函数执行目标代码,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出。程序运行结束后,shell会回收hello进程,并且内核会从系统中删除hello相关痕迹,至此,hello完成O2O的过程
1.2 环境与工具
硬件环境:2.9 GHz Intel Core i7 CPU; 16 GB 2133 MHz LPDDR3内存; 500G Macintosh HD
软件环境:MacOS 64位; Parallels Desktop虚拟机; Ubuntu 18.04.2 LTS
开发工具:GDB/OBJDUMP;EDB;gcc;readelf;gedit;codeblocks
1.3 中间结果
hello.i: hello.c经预处理后生成hello.i
hello.s: hello.i经编译后的形成的汇编程序hello.s
hello.o: hello.s经汇编后的形成的可重定位目标文件hello.o
hello: hello.o经过链接的形成的可执行目标文件hello
elf.txt: hello.o查看elf信息生成的相关文本。
hello_out.elf: hello的elf格式
1.4 本章小结
本章介绍了hello.c程序P2P、020的过程,以及实验过程中所用到的硬软件开发环境和使用的调试工具,撰写报告过程中生成的中间结果

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
·预处理的概念:以符号“#”开头的为预处理命令,在编译之前进行处理,修改程序。例如前三行#include xxx读取头文件并将其插入程序中王梓硕
·C语言的预处理主要有三个方面的内容;
1.宏定义
2.文件包含
3.条件编译
·作用:
1.#define:预处理(预编译)工作也叫做宏展开:将宏名替换为文本(这个文本可以是字符串、可以是代码等)
2.#include<>:修改文件后,编译时就以包含处理以后的文件为编译单位,被包含的文件将作为源文件的一部分被编译。
3.有多种格式。#ifdef #else #endif等有些语句希望在条件满足时才编译预编译使问题或算法的解决方案增多,有助于我们选择合适的解决方案。
预处理不进行语法检查等操作
2.2在Ubuntu下预处理的命令
使用命令gcc -m64 -no-pie -fno-PIC -E -o hello.i hello.c 得到hello.i

2.3 Hello的预处理结果解析
对前三行(不算注释)#include处理:
图片为stdio.h stdlib.h的处理的体现,用绝对路径将其替代
可以发现main函数前的寥寥数行已经被扩展成了数千行

hello.i中
还包括标准C库中数据类型的声明(typedef)
结构体的定义(struct)
引用的外部函数的声明(extern xxxxx)

2.4 本章小结
本章中对hello.c预处理至hello.i的过程进行了讨论,介绍了预处理的概念和作用,并对预处理的结果hello.i文件中的相关内容进行了解析。main函数前的寥寥数行扩展成数千行就是预处理实现的,是后续程序操作的基础
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
·编译的概念:此处编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。即通过编译器ccl,将文本文件.i编译成文本文件.s,
·作用:将高级语言转换成低级机器语言指令。它包含了一个汇编语言程序。不同高级语言经过编译器编译后,都会输出为同一汇编语言。编译器将会对程序进行优化(-Og等)、语法检查等
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成
3.2 在Ubuntu下编译的命令
使用命令gcc -m64 -no-pie -fno-PIC -S -o hello.s hello.i 得到hello.s

3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
我将采取代码段从上至下按执行顺序进行分析
hello.s中代码由三个不同的元素组成:
·指示(Directives) 以点号开始,用来指示对编译器,连接器,调试器有用的结构信息。指示本身不是汇编指令。例如,.file 只是记录原始源文件名。.rodata表示数据段(section)的开始地址, 而 .text 表示实际程序代码的起始。.string 表示数据段中的字符串常量。 .globl main指明标签main是一个可以在其它模块的代码中被访问的全局符号 。至于其它的指示你可以忽略。
·标签(Labels) 以冒号结尾,用来把标签名和标签出现的位置关联起来。例如,标签.LC0:表示紧接着的字符串的名称是 .LC0. 标签main:表示指令 pushq %rbp是main函数的第一个指令。按照惯例, 以点号开始的标签都是编译器生成的临时局部标签,其它标签则是用户可见的函数和全局变量名称。
·指令(Instructions) 实际的汇编代码 (pushq %rbp), 一般都会缩进,以便和指示及标签区分开来。

3.3.1先说一下开头几行:(main函数前) 指示部分

.file:源文件名
.text:代码段
.section .rodata:定义内存段类型为.rodata
.align:对齐方式
.string:字符串
.globl:全局符号
.type:指定是对象类型或是函数类型

3.3.2:变量类型
·int i;局部变量,通过指向栈底的寄存器偏移量间接寻址获得,存放在%rdi与%rsi当中。根据movl $0, -4(%rbp)操作可知i的数据类型占用了4字节的栈空间

·字符串:printf传入的格式化参数,待打印的string类型串“Hello 1180400404”存放在.string节中。字符串使用UTF-8的格式编码的,一个汉字在UTF-8中占三个字节
Hello %s %s\n”,仍然是由printf函数传入的格式化参数
可以看到,两个字符串都被存放在.rodata段,作为全局变量
·int argc, char argv[] argc是参数数量,argv是参数表数组。参数局部变量存放在栈中,用寄存器存放地址,相应地指向该地址单元如下图。栈中一般为char[]分配了一段连续的空间来存放相关内容,并且数组一般都是从栈底指针开始分配。函数传参的时候按逆序将参数压入栈中,这样取参数就是正序(只需要将rsp指针上移即可)

3.3.3:main函数 (整个int main()中的全部执行过程)

首先执行了pushq %rbp和movq %rsp, %rbp。压栈
向一个函数传递参数时,根据参数的顺序,从前往后分别由%rdi、%rsi、%rdx、%rcx、%r8和%r9进行(更多的参数通过栈来传递)。
subq $32, %rsp:在栈中分配出大小为32字节的空间,用来存放被调用函数中所建立的局部变量
movl %edi, -20(%rbp) 这两行传递参数int main(int argc,char argv[])
movq %rsi, -32(%rbp)
%edi中存放的是参数argc,%rsi中存放的是参数
argv[],并将这两个参数的内容存放在%rbp-20和%rbp-32中。
cmpl $4, -20(%rbp) 关系操作:条件判断,if(argc!=4)(传入参数数量为4)
je .L2。 控制转移:若相等,跳转到.L2
不相等时(满足判断条件):执行printf
movl $.LC0, %edi 参数传递(string中内容)
call puts 打印
movl $1, %edi 参数传递(1)
call exit 调用 exit(1)函数
若相等(不满足判断条件,跳过if中语句printf和exit到.L2)

赋值一个0,无条件跳转的.L3。即初始化循环的标志局部变量i=0
再看.L3

cmpl $7, -4(%rbp) 控制循环次数
jle .L4 将%rbp-4处的值(i)与7比较 若小于等于,跳转.L4
若不跳转 执行:
call getchar getchar()清除缓冲区
movl $0, %eax 赋值
leave 出栈
.cfi_def_cfa 7, 8
ret return 0
.cfi_endproc
整体为返回值寄存器初始化movl $0, %eax 然后结束进程

跳转:循环体部分:

共调用了三个函数printf、atoi、sleep
-32(%rbp)为argv[0]的地址。由于数组的空间是连续的,64位系统中char* 数据类型占8个字节, -32(%rbp)赋值给%rax后执行addq $8, %rax,此时存放在%rax中的内容就是数组元素argv[1]所在的地址了。(如果想要得到数组后续元素的值也是累加即可)%rsi和%rdx存放的即为argv[1]和argv[2]
传入printf的值:%edi中.LC1(Hello %s %s\n)
%rsi 中argv[1]
%rdx 中argv[2]
随后call printf函数
同理调用atoi之前将argv[3]作为参数传入,地址为-32(%rbp)再加24
调用sleep前传入atoi函数返回的结果(atoi结果在%eax中,再传给%edi给sleep函数)
最后addl $1, -4(%rbp) 即i++,控制循环

代码中cfi_等未进行解释,详情:
https://sourceware.org/binutils/docs/as/CFI-directives.html
关键字cfi 它是Call Frame infromation的意思.cfi_startproc 和 .cfi_endproc 分别是CFI 的初始过程和结束过程指令,它们隐藏了一些 CFI 有关的操作。ret 是从当前过程中返回的指令。这就是一个最简单的 main 函数内部的三个步骤:CFI 初始操作 – 返回 – CFI 结束操作。由于第一个和最后一个步骤永远伴随着函数,我们大可将注意力集中在这两个步骤之间的代码,也就是 main 函数的实际内容
3.4 本章小结
本章节中进行了hello.i编译至hello.s的过程,并对hello.s中的内容进行了详细解读。更深刻理解了汇编代码。理解了编译产生了什么。对于变量的存放、传入调用、数组的空间连续、条件语句(关系操作),循环(控制转移),赋值等操作。算术操作、函数操作、函数返回等都有了更深的理解。对C语言中的数据与操作有了更深刻的认识

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,这个过程就叫做汇编.
汇编的作用:翻译生成机器语言,以便计算机能直接识别和执行
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
使用命令gcc -m64 -no-pie -fno-PIC -c -o hello.o hello.s

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
使用命令readelf -a hello.o > elf.txt 配合gedit命令

(1)ELF Header:

Elf头以一个16进制的序列(Magic)开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位(Relocatable file)、可执行或者共享的)、机器类型(x86-64)、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息.不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目

(2)Section Headers

节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置、大小、偏移量等信息
(3)Relocation section

.rela.text 保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。
.rela.eh_frame中是.eh_frame节重定位信息。
两种最基本的重定位类型:
R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。
R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。
程序调用的本地函数指令地址属于绝对地址,不需修改重定位后的地址信息。

(4)Symbol table

.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。任何已初始化的全局变量地址或外部函数地址都需要被修改
4.4 Hello.o的结果解析
使用命令objdump -d -r hello.o 对hello.o反汇编
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

机器语言指的是二进制的机器指令集合,而机器指令是由操作码和操作数构成的.汇编语言的主体是汇编指令.汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式.
(1)操作数:反汇编结果中,除了右侧的汇编语言,左侧增加了由操作码和操作数组成的机器指令。通常机器指令的第一个数码为操作码,后续的数码为操作数。hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制(0x…)
(2)分支转移:在反汇编汇编语言中,分支转移时的跳转目标地址为相对偏移量,而原来的.s文件中是.L2, .L3等注记符。因为转换成机器语言,在反汇编之后,注记符不复存在。
(3)访问全局变量、函数调用等时,.s文件中,使用的是注记符,而反汇编文件中是.rodata+偏移量、当前PC+偏移量。即消除了所有的注记符.L1等
4.5 本章小结
本章通过将hello.s编译成hello.o机器语言指令,再通过readelf查看可重定位目标程序文件,通过对elf文件结构的分析,获得相关数据的运行时地址,以及不同节的、条目的大小、偏移量等信息。同时,通过.s文本文件与由机器语言反汇编获得的汇编代码比较,知道并讨论了.s文件中,通过注记符寻址和经反汇编后,重定位表示的地址信息差异。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接的概念:通过链接器,将各种代码和数据片段收集并合成为一个单一文件,并得到hello的可执行目标文件的过程称为链接。该二进制文件可被加载到内存,并由系统执行。
链接的作用:
1.链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。基于此特性的改进,以提高程序运行时的时间、空间利用效率。
2.链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。它将巨大的源文件分解成更小的模块,更易于管理。我们可以单独修改或编译这些模块中的一个或某几个,并重新链接应用,不必再重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
使用命令:
ld -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/gcc/x86_64-linux-gnu/7/crtbegin.o
/usr/lib/gcc/x86_64-linux-gnu/7/crtend.o
/usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello

从hello.c直接生成hello可用命令: gcc -m64 -Og -no-pie -fno-PIC hello.c -o hello
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
采用命令:readelf -a hello > hello_out.elf

可执行目标文件hello的格式类似于可重定位目标文件的格式。
ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。

ELF Header内容解析见第四章 仅在头数量等发生变化

Section Headers 对 hello 中所有的节信息进行了声明,其中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。

比hello.o多出Program Headers:

含义:
PHDR:保存程序头表
INTERP:动态链接器的路径
LOAD:可加载的程序段
DYNAMIN:保存了由动态链接器使用的信息
NOTE保存辅助信息
GNU_STACK:标志栈是否可执行
GNU_RELRO:指定重定位后需被设置成只读的内存区域
程序头表在执行时被使用,它告诉链接器加载的内容,并提供动态链接信息,每个表项提供了各段在虚拟地址空间的大小、偏移量,和物理空间的地址、权限标记、对齐长度

还多出了动态节偏移量、重定位节偏移量等。
可执行文件时完全连接的,所以无.rel节。

还有就是一些Symbol table符号表、版本信息等,不做赘述。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
从Data Dump窗口观察hello加载到虚拟地址的状况,并查看各段信息。

在0x00400000段中,程序被载入,即对应的.init, .text, .rodata, .data, .bss节
自虚拟地址0x400000开始,到0x400fff结束,这之间每个节的地址、大小都同5.3中的地址声明。

程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息,具体内容见5.3

5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。

·除.text t节外,增加了.init, .plt ,.fini段。.init段用于初始化程序执行环境,.plt段是程序执行时的动态链接
·程序添加了许多动态链接库中的函数。使用ld链接,定义了函数入口,初始化函数,动态链接器与动态链接共享库定义hello.o中的各种函数,将上述共享函数加入
_start,deregister_tm_clones,register_tm_clones,__do_global_dtors_aux,frame_dummy,__libc_csu_init,__libc_csu_fini,_fini等函数;
·hello的反汇编代码中函数调用时,call的地址为运行时的绝对地址,而hello.o的反汇编代码中,是重定位条目信息。即所有的重定位条目都被修改为了确定的运行时内存地址。
对比:

部分图片:

·链接过程:为了构造可执行文件,链接器先后完成两个主要任务:符号解析和重定位。每个符号对应一个函数、全局变量、静态变量,通过符号解析,将定义与引用关联起来。
·重定位:以exit函数的重定位过程为例。
由上图,exit函数运行时地址为0x400530。
引用的运行时地址为ADDRESS+ offset = 0000000000400637 + 0x25=0x40065c
引用应当修改的偏移调整为addend = - 0x4

更新该PC相对引用,使其在运行时指向exit函数,*refptr = 0x400530 – 0x40065c -0x4 =-0x12g = 0xfffffed0 同反汇编代码中地址
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。部分代码图片:

表跳转即为跳转,未标为call调用 未step进入函数进行分析
0x7f2e2654bea0 = 0x00007f2e2654bea0 <ld-2.27.so!_dl_start+0>
0x7fd22e659630 = 0x00007fd22e659630 <ld-2.27.so!_dl_init+0>
跳转r12 = 0x0000000000400550 <hello!_start+0>
跳转*0x200a76(%rip) = [0x0000000000600ff0] = 0x00007f022f197ab0 <libc-2.27.so!__libc_start_main+0>
0x4004f0 = 0x00000000004004f0 <hello!puts@plt+0>
0x400530 = 0x0000000000400530 <hello!exit@plt+0>
不跳:(不进入if)
跳转0x4006af = 0x00000000004006af <hello!main+120>
跳转0x400669 = 0x0000000000400669 <hello!main+50>
0x400500 = 0x0000000000400500 <hello!printf@plt+0>
0x400520 = 0x0000000000400520 <hello!atoi@plt+0>
0x400540 = 0x0000000000400540 <hello!sleep@plt+0>
循环跳回0x400669 = 0x0000000000400669 <hello!main+50>
0x400510 = 0x0000000000400510 <hello!getchar@plt+0>
下面两行为getchar内部代码
0x200b24(%rip) = [0x0000000000601010] = 0x00007fcaecf89750 <ld-2.27.so!_dl_runtime_resolve_xsavec+0>
0x7fcaecf81df0 = 0x00007fcaecf81df0 <ld-2.27.so!_dl_fixup+0>
0x20(%rbp) = [0x00007fcaecf692c0] = 0x00007fcaecc0e020 <libc-2.27.so!__GI__IO_file_underflow+0> 行edb莫名卡住
改换gdb反汇编
00000000004010c0 <_dl_relocate_static_pie>
00000000004010d0 <deregister_tm_clones>
0000000000401100 <register_tm_clones>
0000000000401140 <__do_global_dtors_aux>
0000000000401170 <frame_dummy>
0000000000401172
0000000000401200 <__libc_csu_init>
0000000000401260 <__libc_csu_fini>
0000000000401264 <_fini>
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,
通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

动态链接的基本思想是把程序按照模块拆分成各个相对独立的部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件,更加节省内存并减少页面交换

关注_GLOBAL_OFFSET_TABLE一项

在dl_init之前:0x00601000开始的global_offset表是全0的状态

执行_dl_init后被赋上了相应的偏移量的值

说明dl_init操作是给程序赋上当前执行的内存地址偏移量,初始化hello程序
5.8 本章小结
本章从ld链接器将hello.o的链接命令,分析可执行文件 hello 的 ELF 格式及其虚拟地址空间,以及hello的重定位过程,执行流程,hello的动态链接分析,进一步加深了对链接过程细节的理解。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:一个执行中的程序的实例,同时也是系统进行资源分配和调度的基本单位。一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
进程的作用:给予应用程序关键抽象:
(1)程序好像独占地使用处理器和内存
(2)程序中的代码和数据好像是系统内存中唯一的对象
6.2 简述壳Shell-bash的作用与处理流程
shell-bash 的作用:shell-bash 是一个 C 语言程序,是用户使用 Unix/Linux 的 桥梁,它交互性地解释和执行用户输入的命令,能够通过调用系统级的函数或功能 来执行程序、建立文件、进行并行操作等等。同时它也能够协调程序间的运行冲突,保证程序能够以并行形式高效执行。bash 还提供了一个图形化界面,提升交互的速度。
shell-bash 的处理流程:
(1)从终端或控制台获取用户输入的命令
(2)对读入的命令进行分割并重构命令参数
(3)如果是内部命令则调用内部函数来执行,否则为其分配子进程,执行外部程序
(4)判断程序的执行状态是前台还是后台,若为前台进程则等待进程结束;否则直接将进程放入后台执行,继续等待用户的下一次输入。即接受信号;中断处理
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
父进程和新创建的子进程最大的区别在于他们有不同的id。Fork只被调用一次但是返回两次,在父进程中fork会返回子进程的PID,在子进程中fork会返回0。可以通过返回值来区分程序是在父进程还是在子进程中执行。

6.4 Hello的execve过程
execve的功能是在当前进程的上下文中加载并运行一个新程序。fork 之后,shell 在子进程中调用 execve 函数
execve 调用驻留在内存中的被称为启动加载器的操作系统代码来 执行 hello 程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数 据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容,然后跳转到_start,_start 函数调用系统启动函数__libc_start_main 来初始化环境,调用hello 的 main 函数,并在需要的时候将控制返回给内核。
注意:在加载过程中并没有从磁盘到内存的数据复制,直到CPU引用一个被映射的虚拟页时,才进行复制,此时,操作系统利用他的页面调度机制自动将页面从磁盘传送到内存。
execve函数不同于fork函数,execve函数只有在找不到文件时才会返回,否则调用一次,不返回

main的栈结构:

6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
·操作系统内核使用一种称为“上下文切换”的较高层形式的异常控制流来实现多任务:内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程一打开文件的信息的文件表。
上下文切换的流程是:1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。
hello 在刚开始运行时内核为其保存一个上下文, 进程在用户状态下运行。如果没有异常或中断信号的产生,hello将继续正常地执行。如果有异常或系统中断,那么内核将启用调度器休眠当前进程,并在内核模式中完成上下文切换,将控制传递给其他进程。sleep、getchar函数调用时执行上下文切换。把控制转移给其他进程,结束后(计时器到时或缓冲区清除完成),引发中断信号,切回到hello进程。最后return结束进程

·时间分片:各个进程是并发执行的,每个进程轮流在处理器上执行,一个进程执行它控制流的一部分成为时间分片。
·用户模式和内核模式:处理器为了安全起见,不至于损坏操作系统,必须限制一个应用程序可执行指令能访问的地址空间范围。就发明了两种模式用户模式和内核模式,其中内核模式(上帝模式)有最高的访问权限,甚至可以停止处理器、改变模式位,或者发起一个I/O操作,处理器使用一个寄存器当作模式位,描述当前进程的特权。进程只有当中断、故障或者陷入系统调用时,才会将模式位设置成上帝模式,得到内核访问权限,其他情况下都始终在用户权限中,就能够保证系统的绝对安全。
内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度。当内核调度了一个新的进程运行后,它就抢占hello进程,并且使用上下文切换机制来将控制转移到新的进程。
hello进程初始运行在用户模式中,直到hello进程中的sleep系统调用。它显式地请求让Hello进程休眠,内核可以决定执行上下文切换,进入到内核模式。当定时器到时后引发中断信号,内核就能判断sleep休眠完毕,切换回用户模式。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理

终止:产生SIGINT信号,程序的运行被终止
中断:产生SIGSTP信号,程序的运行被挂起

·正常执行的hello程序 程序执行完后,进程被回收。按回车键结束

·运行时乱按。乱按的输入并不会影响进程的执行,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其他字串被当做 shell 命令行输入。

·在程序输出2条后按下Ctrl-Z

按下Ctrl+Z后,父进程收到SIGTSTP信号,hello进程被挂起。使用ps命令列出当前系统中的进程(包括僵死进程)。

我们可以看出 hello 进程没有被回收。
使用jobs命令。此时其后台的job号为1

使用fg 1将其调到前台,此时 shell 程序首先打印 hello 的命令行命令,之后继续运行打印剩下的6条,再键入回车,程序结束,同时进程被回收。

我们也可以在打印两行后ps查询具体pid信息,然后调用kill,发送SIGKILL信号给指定的pid杀死指定的进程

·在程序输出2条后按下Ctrl-C

内核向前台进程发送一个SIGINT信号,前台进程终止,内核再向父进程发送一个SIGCHLD信号,通知父进程回收子进程,此时子进程不再存在
pstree命令:以树状图显示进程间的关系 一下是pstree部分截图

6.7本章小结
本章从进程的概念开始,讲述了shell-bash的概念、作用,随后阐述了hello从被父进程的fork创建,再被execve加载,再到通过内核模式控制下的上下文切换,来实现hello进程以时间分片的形式并发执行的过程。最后通过hello执行过程中可能发生的异常,以及信号处理方式的实际操作实践,切身感受了各种各样的异常处理过程。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
·逻辑地址:一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量, 表示为 [段标识符:段内偏移量。如hello.o里面的相对偏移地址
23:8048000 段寄存器(CS等16位):偏移地址(16/32/64)
实模式下: 逻辑地址CS:EA =物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,
段地址+偏移地址=线性地址。
·线性地址:逻辑地址经过段机制后转化为线性地址,表示为[描述符:偏移量]。是逻辑地址到物理地址变换之间的中间层。分页机制中线性地址作为输入。
是非负整数地址的有序集合 {0, 1, 2, 3 … }
·虚拟地址:CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。即hello的虚拟内存地址。
N = 2n 个虚拟地址的集合 ===线性地址空间 {0, 1, 2, 3, …, N-1}
·物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机主存被组织成由M个连续字节大小的内存组成的数组,每个字节都有一个唯一的地址,该地址被称为物理地址。对于hello来说是在其运行时由MMU根据虚拟内存映射到的物理地址。
M = 2m 个物理地址的集合 {0, 1, 2, 3, …, M-1}

Intel采用段页式存储管理(通过MMU实现)
段式管理:逻辑地址->线性地址==虚拟地址
页式管理:虚拟地址->物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
由7.1可知:逻辑地址空间表示:段地址:偏移地址。 在实模式下:逻辑地址CS:EA=CS*16+EA 物理地址。在保护模式下:以段描述符作为下标,到 GDT/LDT 表查表获得段地址,段地址+偏移地址=线性地址。段内偏移量是在链接后就已经得到的32位地址,因此要想由逻辑地址得到线性地址,需要根据逻辑地址的前 16 位获得段地址,这 16 位存放在段寄存器中。
段寄存器(16 位):用于存放段选择符 CS(代码段):程序代码所在段 SS(栈段):栈区所在段 DS(数据段):全局静态数据区所在段其他三个段寄存器 ES、GS 和 FS 可指向任意数据段。

段选择符中字段的含义:

其中 CS 寄存器中的 RPL 字段表示 CPU 的当前特权级 TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
RPL=00 为第 0 级,位于最高级的内核态;RPL=11 为第 3 级,位于最低级的用户态高13 位-8K个索引用来确定当前使用的段描述符在描述符表中的位置。
段描述符是一种数据结构,等价于段表项,分为两类。一类是用户的代码段和数据段描述符,一类是系统控制段描述符。
描述符表:实际上为段表,由段描述符(段表项构成)分为三种类型:
全局描述符表 GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及 TSS(任务状 态段)等都属于 GDT 中描述的段
局部描述符表 LDT:存放某任务(即用户进程)专用的描述符
中断描述符表 IDT:包含 256 个中断门、陷阱门和任务门描述符
下图为逻辑地址向线性地址转换:

被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址
GDT首址或LDT首址都在用户不可见寄存器中
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存作为缓存的工具,概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组.
磁盘上数组的内容被缓存在物理内存中 (DRAM cache)
这些内存块被称为页 (每个页面的大小为P = 2p字节)
虚拟页面地集合被分为三个不相交的子集:已缓存、未缓存和未分配。
虚拟内存系统通过将虚拟内存分割为大小固定的“虚拟页”来处理问题。同时物理内存也被分割为大小等同与虚拟页的“物理页”,并与“虚拟页”之间建立映射关系。从而达到由线性地址(虚拟地址)到物理地址的变换
图示如下:

根据虚拟页号在当前进程的物理页表中找到对应的页面,若符号位设置为 1,则表示命中,从页面中取出物理页号+虚拟页偏移量即组成了一个物理地址;否则表示不命中,产生一个缺页异常,需要从磁盘中读取相应的物理页到内存。
整体过程为:

虚拟地址分为两部分:前一部分为虚拟页号,可以索引到当前进程的的物 理页表地址,后一部分为虚拟页偏移量,将来可以直接作为物理页偏移量,页表是一个存放在物理内存中的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表
7.4 TLB与四级页表支持下的VA到PA的变换
图片为Core i7 地址翻译过程(VA48位PA52位)

Core i7采用四级页表的层次结构。每个进程有它自己私有的页表层次结构。当一个Linux进程在运行时,虽然core i7体系结构允许页表换进换出,但是与已分配了的页相关联的页表都是驻留在内存中的。
CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。

7.5 三级Cache支持下的物理内存访问
Core i7 MMU使用四级页表来将虚拟地址翻译成物理地址,我们得到了物理地址PA。现在分析三级cache支持下的物理内存访问
首先取组索引对应位,向L1cache中寻找对应组。如果存在,则比较标志位,并检查对应行的有效位是否为1。如果上述条件均满足则命中,根据块偏移CO返回数据。否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后向上级cache返回直到L1cache。如果有空闲块则直接将目标块放置到空闲块中,否则必须将缓存中的某个块驱逐,将目标块放到被驱逐块的原位置。一般采用最近最少被使用策略LRU进行替换。

Intel Core i7 内存系统图:

7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行新程序a.out的步骤:
·删除已存在的用户区域
·创建新的区域结构
私有的、写时复制
代码和初始化数据映射到.text和.data区(目标文件提供)
.bss和栈堆映射到匿名文件 ,栈堆的初始长度0
·共享对象由动态链接映射到本进程共享区域
·设置PC,指向代码区域的入口点
Linux根据需要换入代码和数据页面
如图所示:

7.8 缺页故障与缺页中断处理
Page fault缺页: 虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)
缺页故障:当CPU发送一个VA,发现对应的物理地址不在内存中,必须从磁盘中取出,此时就会发生故障,缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。
缺页中断处理:
1.处理器生成一个虚拟地址,并把它传送给MMU.
2. MMU生成PTE地址,并从高速缓存/主存请求得到它.
3.高速缓存/主存向MMU返回PTE.
4.PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序.
5.缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘.
6.缺页处理程序页面调人新的页面,并更新内存中的PTE.
7.缺页处理程序返回到原来的进程,再次执行导致缺页的指令,CPU将地址重新发送给MMU.因为虚拟页面现在已经缓存在物理内存中,所以会命中,主存将所请求字返回给处理器
图示:
VM缺页(之前),对VP3中的字的引用不命中,从而触发缺页

缺页异常处理程序选择一个牺牲页 (VP 4),并分配一个新的虚拟页面,内核在磁盘上分配VP5,并且将PTE5指向这个新的位置

7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。例如C语言中的 malloc 和 free
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage collection)。
一、带边界标记的隐式空闲链表
(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本章小结
本章通过对虚拟内存的了解,从段式管理到页式管理出发,较为详细地讨论了虚拟地址到物理地址的变换过程。学会了TLB和四级页表支持下VA到PA的转换,以及得到了PA后,三级cache下的物理内存的访问过程。同时,对进程在调用fork和exceve函数时的内存映射进行了回顾,更深入掌握了fork函数和exceve函数和虚拟内存的种种联系,总结了发生缺页故障时的处理步骤。还学会了动态内存分配的管理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个 Liunx 文件就是一个 m 个字节的序列:B0,B1,…,Bm-1。所有的 I/O 设备都被模型化为文件。
现实情况: 所有的I/O设备都被模型化为文件:
/dev/sda2(用户磁盘分区)
/dev/tty2(终端)
甚至内核也被映射为文件:
/boot/vmlinuz-3.13.0-55-generic(内核映像)
/proc (内核数据结构)
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O: 这使得所有的输入和输出都能以一种统一且一致的方式来执行。
文件的类型(每个Linux文件都有一个类型(type)来表明它在系统中的角色):
1.普通文件:包含任何数据,分两类
·文本文件:只含有 ASCII 码或 Unicode 字符的文件
·二进制文件:所有其他文件
2.目录:包含一组链接的文件。每个链接都将一个文件名映射到一个文件
3.套接字:用于与另一个进程进行跨网络通信的文件
8.2 简述Unix IO接口及其函数
Unix IO接口:
·打开文件,内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
Linux内核创建的每个进程都以与一个终端相关联的三个打开的文件开始:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
·改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。

·读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
·关闭文件。当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
Unix IO函数:
1.open():

返回一个小的描述符数字---- 文件描述符。返回的描述符总是在进程中当前没有打开的最小描述符。
fd == -1 说明发生错误
2.close():

注意检查返回码,避免错误
3.read():

返回值表示的是实际传送的字节数量
返回类型 ssize_t 是有符号整数
nbytes < 0 表示发生错误
不足值(Short counts) (nbytes < sizeof(buf) ) 是可能的,不是错误
4.write()

返值表示的是从内存向文件fd实际传送的字节数量
nbytes < 0 表明发生错误
同读文件一样, 不足值(short counts) 是可能的,并不是错误!
5.seek()
用于在指定的文件描述符中将将文件指针定位到相应位置
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
返回值:成功:返回当前位移;失败:返回-1

8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

查看 printf 代码:

int printf(const char fmt, …)
{ int i;
char buf[256];
va_list arg = (va_list)((char)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}

fmt是一个指针,这个指针指向第一个const参数(const char fmt)中的第一个元素。由于栈是从高地址向低地址方向增长的,可知(char)(&fmt) + 4) 表示的是第一个参数的地址。
查看 vsprintf 代码:

int vsprintf(char *buf, const char fmt, va_list args)
{
char p;
char tmp[256];
va_list p_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != ‘%’) //忽略无关字符
{
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt)
{
case ‘x’: //只处理%x一种情况 itoa(tmp, ((int)p_next_arg)); //将输入参数值转化为字符串保存在tmp
strcpy(p, tmp); //将tmp字符串复制到p处 p_next_arg += 4; //下一个参数值地址
p += strlen(tmp); //放下一个参数值的地址 break;
case ‘s’:
break;
default:
break;
}
}
return (p - buf); //返回最后生成的字符串的长度
}
则知道 vsprintf 程序按照格式 fmt 结合参数 args 生成格式化之后的字符串,并返回字串的长度。 在 printf 中调用系统函数 write(buf,i)将长度为 i 的 buf 输出。
write 函数如下
write: mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在 write 函数中,将栈中参数放入寄存器,ecx 是字符个数,ebx 存放第一个字符地址,int INT_VECTOR_SYS_CALLA 代表通过系统调用 syscall
查看 syscall 的实现
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret

syscall 将字符串中的字节“Hello 1180400404 王梓硕 2”从寄存器中通过总线复
制到显卡的显存中,显存中存储的是字符的 ASCII 码。
字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存 储到 vram 中。
显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。于是我们的打印字符串“Hello 1180400404 王梓硕”就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar函数内容:

getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
8.5本章小结
本章学会了linux下IO设备的管理方法,了解了Unix IO和Unix IO函数,深入分析了printf函数和getchar函数的实现
(第8章1分)

结论
没有学习本课程之前很难想象一个简单的hello程序背后有如此多复杂又精细的机制。接下来对hello从开始到结束重新进行梳理
1.hello.c借助IO进行编写并储存在主存中。
2.cpp将hello.c预处理为hello.i文件
3.cll将hello.i编译为hello.s汇编文件
4.as将hello.s汇编为可重定位目标文件hello.o
5.ld将hello.o和外部文件链接成可执行文件hello
6.在shell输入命令后,通过exceve加载并运行hello,为其创建虚拟内存映像,进入程序入口后开始载入物理内存,进入main函数
7.CPU为hello分配时间片,顺序执行逻辑控制流
8.hello的VA通过TLB和页表翻译为PA
9.三级cache 支持下的hello物理地址访问
10.printf会调用malloc通过动态内存分配器申请内存
11.hello在运行过程中遇到各种异常和信号等
12.shell父进程回收hello子进程,内核删除为hello创建的所有数据结构,hello的一生结束了
(结论0分,缺失 -1分,根据内容酌情加分)

附件

  1. hello.c:源代码
  2. hello.i:hello.c经预处理后hello.i
  3. hello.s:.hello.i经编译后的形成的汇编程序hello.s
  4. hello.o:hello.s经汇编后的形成的可重定位目标文件hello.o
  5. hello:hello.o经过链接的形成的可执行目标文件hello
  6. elf.txt:hello.o查看elf信息生成的相关文本。
  7. hello_out.elf:hello的elf格式文件。

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

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://blog.csdn.net/pro_technician/article/details/78173777
[2] https://www.cnblogs.com/justinyo/archive/2013/03/08/2950718.html
[3] https://blog.csdn.net/weixin_43821874/article/details/86485888
[4] https://blog.csdn.net/jdjaha/article/details/103758042
[5] 老师课件ppt
[6] https://blog.csdn.net/wkwk7600/article/details/83418109
[7] 百度百科
[8] 《深入理解计算机系统》第三版
[9] https://baike.baidu.com/item/argc%20argv/10826112?fr=aladdin
[10] https://www.cs.stevens.edu/~jschauma/631/elf.html
[11] https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)

CSF2020大作业相关推荐

  1. java大作业私人管家系统_操作系统概念(Operating System Concepts)第十版期中大作业...

    更正: 第一题中,哲学家就餐问题中的哲学家的状态state[i]应属于临界区变量,是可能会产生读写冲突的,所以对其进行读写的时候均需要加一把互斥锁. 非常感谢不听不听不听的指正. ---------- ...

  2. 大一c语言大作业课题大全,昆明理工大学大一C语言大作业题目.doc

    昆明理工大学大一C语言大作业题目 综合性实践排序求平均值(包括将数拆散求最大最小值).函数ReadDat()随机产生100个存放到数组aa中00个jsSort()函数的功能是:进行降序排列.最后调用函 ...

  3. 清华贵系的期末大作业:奋战三周,造台计算机!

    大数据文摘授权转载自AI科技评论 作者 | 蒋宝尚 编辑丨陈彩娴 本科大三,正在学习计算机组成原理,能做个什么项目? 清华大学贵系说:造台计算机吧! 清华有门本科三年级必修课,名为<计算机组成原 ...

  4. 清华大作业指导:一人单刷雨课堂需要多少工作量?快手工程师详解如何两周搞定...

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 本文转载自:机器之心 昨天,清华自动化大一学生的 C++大作业霸占了 ...

  5. 大TTT需要复习的课件PPT以及大作业完成链接

    jsp课件:https://download.csdn.net/download/weixin_42859280/11243121 matlab课件:https://download.csdn.net ...

  6. JSP大作业数据库_本地MySQL【种种问题】

    JSP大作业数据库MySQL第1部分.zip: 链接:https://pan.baidu.com/s/1ZHwxAnATQSSjC-F6EpxeUw 提取码:30kw JSP大作业数据库MySQL第二 ...

  7. Linux综合大作业

    1.手动添加一个用户ata 2.用Vi修改/ect/password,复制root用户的信息,粘贴到文件的最后一行 3.修改最后一行的内容为普通用户ata的信息 4.添加一个用户,帐号为testata ...

  8. 1008c语言答案,c语言大作业题目01008.doc

    c语言大作业题目01008 一.学生信息管理程序 基本要求: 1.要求实现学生信息的查找.添加.删除.修改.浏览.保存.从文件读取.查看奖学金信息8个功能,每个功能模块均能实现随时从模块中退出,而且可 ...

  9. 华南理工大学计算机操作系统课程设计大作业银行家死锁避免算法模拟,2016春操作系统大作业银行家死锁避免算法模拟.doc...

    文档介绍: 2016春操作系统大作业银行家死锁避免算法模拟20160501华南理工大学"计算机操作系统"课程设计大作业计算机科学与技术专业:春2015班级:号:2015047420 ...

  10. 【大作业】城市地铁线路最短路规划及路径输出(满分)

    4.地铁搭乘方案选择 轨道交通越来越发达,我们的出行也越来越方便.从学校门口的地铁站乘坐地铁可以到达很多地方. 计算地铁出行的最短路径,要求如下:(默认站点与站点之间权值为 1,也可以用时间或距离进行 ...

最新文章

  1. 126. Leetcode 剑指 Offer 46. 把数字翻译成字符串 (动态规划- 字符串系列)
  2. 搜索引擎学习(六)Query的子类查询
  3. Future和FutureTask实现异步计算
  4. android 自定义控件 焦点,Android 自定义Button按钮显示样式(正常、按下、获取焦点)...
  5. STM32 GPIO应用
  6. 免费计算机网络基础ppt,计算机网络基础
  7. iphone用计算机显示器,如何将iPhone屏幕投射到电脑上
  8. 进展:Pegasus的自动化编译测试
  9. python语言实现读取菜谱_通过Python语言实现美团美食商家数据抓取
  10. workman 搭建tcp服务器,和websocket互相通信
  11. 适合所有手环的app_Redmi Watch体验:手环终结者?
  12. 麒麟子Javascript游戏编程零基础教程大纲
  13. 华为 AI 芯片诞生;马云重当中国首富;微软修复数据删除 Bug | 极客头条
  14. JSONObject排序问题
  15. pytorch学习 -- 反向传播backward
  16. Influxdb相关概念及简单实用操作
  17. Shader实现马赛克
  18. Mongodb学习(1)安装以及配置
  19. 01组团队项目-Alpha冲刺-5/6
  20. AJAX技术学院风连衣裙,清新又减龄学院风连衣裙,轻松穿出少女感

热门文章

  1. Oracle中文乱码(中文变问号?)解决方法---简单粗暴高效
  2. 计算机ps特效教程,制作木质电脑桌的PS滤镜教程
  3. 源码解析-为什么引入了jackson-dataformat-xml 包我的接口全变成了xml格式?
  4. 从软件的价值体系开始向技术的反向分析
  5. 项目实战第二十一讲:平台商品库
  6. Arcgis操作系列一:shp矢量数据的面积计算
  7. 性能碾压Notepad++的文本编辑器UltraEdit,程序员必备
  8. R语言中经纬度度分秒转小数
  9. 计算机专业论文答辩ppt,计算机毕论文答辩PPT(完整版).ppt
  10. C++ 重制植物大战僵尸(Cocos2dx开源项目)