程序人生-Hello’s P2P

摘  要

对于每个程序员来说,Hello World是一个开始,本论文主要研究hello从开始执行到结束的整个生命周期,经过对hello生命周期的各个阶段的分析,让我们更加了解计算机的工作原理。

关键词:进程,预处理,编译,汇编,链接,进程管理,存储管理,IO管理;                           

目  录

 

第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:P2P过程就是hello从源程序hello.c经过预处理,编译,汇编,链接等过程生成可执行称程序hello.o的过程

O2O:shell执行可执行文件,管理hello进程,为其映射虚拟内存,分配物理内存,然后将其结果输出到显示器,最后回收内存空间。

1.2 环境与工具

硬件环境:x86-64  8GRAM 

软件环境:deepin15.11 64位

使用工具:gcc,objdump,gdb,edb,Hex Editor

1.3 中间结果

Hello.i 预处理文本

Hello.s编译得到的文本程序

Hello.o 汇编得到的二进制文本程序

Hello 链接生成的可执行二进制文件

Hello.txt   hello.o文件objdump反汇编生成的汇编文本程序

Hello1.txt   hello文件的objdump反汇编生成的汇编文本程序

Hello.elf   hello.o文件的elf格式。

Hello1.elf  hello.o文件的elf格式。hello.o文件的elf格式。hello.o文件的elf

1.4 本章小结

 

本章主要对hello程序进行了一些简单的介绍,介绍了本论文操作时所用的硬件,软件平台以及一些处理工具,同时对每个阶段产生的文件做了一些介绍。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:在编译之前进行的处理。 C语言的预处理主要有三个方面的内容: 1.宏定义; 2.文件包含; 3条件编译。 预处理命令以符号“#”开头。如下图

预处理的作用:预处理可以完成很多功能,常用的有处理文件包含,例如我们在程序里面写#include<stdio.h>,预处理程序就把stdio.h文件复制一份插入到程序里面。另外一个常用的处理是宏定义,例如源程序里面有#define PI 3.14语句,预处理就把程序里面所有的PI进行替换。便于编译。

2.2在Ubuntu下预处理的命令

预处理命令:gcc -E -o hello.i hello.c

2.3 Hello的预处理结果解析

结果:生成hello.i文件

hello.c文件

Hello.i文件

分析:

可以明显看到与hello.c相比hello.i的代码增加了3000多行,而且前面#的内容发生了变化;前面增加的文本其实是 <stdio.h><unistd.h> <stdlib.h>三个头文件的源码

1用于描述运行库在计算机中的位置

2声明可能用到的函数名

2.4 本章小结

 本章介绍了hello.c的预处理阶段,得到了经过预处理后的hello.i,通过对hello.i进行分析,说明了预处理过程中执行的一些操作,即将头文件的源码插入到hello.i中。

第3章 编译

3.1 编译的概念与作用        

编译的概念:广义上来说编译时将某一种程序设计语言写的程序翻译成等价的另一种语言的程序的程序的过程,而这里指的是将高级语言翻译成汇编代码的过程

编译的作用:编译是把高级语言转变为二进制代码的必经之路,他把高级语言生成汇编语言,使生成过程更加方便流畅

 

3.2 在Ubuntu下编译的命令

编译命令:gcc -S -o hello.s hello.i

3.3 Hello的编译结果解析

编译结果:生成hello.s文件

 

Hello.s分析

1数据类型

  1. 字符串:“用法: Hello 学号 姓名 秒数!\n”

第一个printf中的字符串在hello.s中被编码为UTF-8的编码格式

“Hello %s %s\n”

第二个printf传入的输出格式化参数,在hello.s中声明如上图

  1. 整型数 (hello.c中包含的数据类型有字符串,整型数,和数组)

Int i :Hello.s中申请了整型变量i,该变量是一个局部变量.编译器将局部变量存储在寄存器或栈中,在hello.s中编译器将i存储在-4(%rbp)中可以看出i占4个字节

 

Int argc:储存在寄存器%edi中,作为参数传入到-20(%rbp)的内存位置

立即数:hello.s中的$0,$8等都是立即数,直接硬编码在汇编代码中

  1. 数组

char *argv[]:argv是一个存放指针的数组,是第二个传入的参数

图1:获取argv[1]的值并存入寄存器rdx中

图2获取argv[2]的值并存入到寄存器rax中

图2

图3获取寄存器argv[3]的值并将其作为函数atoi的参数

(以下格式自行编排,编辑时删除)

(第3章2分)

图3

2赋值操作

Hello.s中的赋值操作:i= 0

该语句用movl指令来完成;0以立即数的形式存储,将0送给寄存器eax。赋值完成。

3算数操作

Hello.s中的算数操作:

1)i++,

使用addl指令来完成,addl $1, -4(%rbp);如下图

  1. movq     -32(%rbp), %rax

该语句执行的操作是将 %rbx-32的值赋给%rax,如下图

4:关系操作

  1. argc!=4:用cmpl指令对4和存放在-20(%rbp)中的argc进行比较,相等则跳转,否者继续执行。Cmpl指令的功能相当于减法指令,不过 不保存结果只是对标志寄存器产生影响,只需要对标志寄存器进行判断。

  1. i<8:用cmpl指令对7和存放在-4(%rbp)中的i进行比较,小于等于则执行循环体,

5控制转移

  1. if(avgc!=4)

使用cmp指令比较avgc与4,如果相等则使用je语句直接跳转到L2如果不相等,则顺序执行。

  1. For(i = 0;i<8;i++)

先给在L2给i赋值,然后跳转到L3,L3中进行比较如果满足i<=7则进入循环,否则循环结束,调到循环外的下一条语句。

6函数操作

  1. main函数:

函数调用:main函数要被调用才能执行,调用使用call指令,call指令将下一条指令地址压栈,然后跳转到main函数

参数传递:main函数调用了argc 和argv两个参数,分别用%rsi和%rdi存储;(见上文数据类型)。

函数返回:函数的返回值为0.

2)printf函数:在hello.s中printf被替换成了puts

参数传递:第一个printf将字符串的首地址传给了%rdi,然后调用了puts函数。第二个printf将“Hello %s %s\n”的首地址传给了%rdi,设置%rsi为argv[1],%rdx为argv[2]。

2)exit函数(函数调用)

参数传递:将立即数1传给%edi

  1. atoi函数(函数调用)

参数传递:将%rdi设置为1

函数返回:将要返回的值存储在%eax中,然后用ret返回,

  1. sleep函数(函数调用)

参数传递:将%edi设置为atoi(argv[3])(即调用atoi函数返回的数据).

  1. getchar函数(函数调用)

 

 

3.4 本章小结

本章介绍了编译的过程与方法,并对编译得到的文件hello.s做了详细的分析,对hello中涉及的数据类型,函数调用,关系操作,控制转移操作进行了详细的描述,编译为接下来生成机器代码奠定了基础。

 

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编是指将汇编语言翻译成机器语言的过程,这里还包括将这些机器语言指令打包成可重定位的目标程序的过程。

汇编的作用:把汇编语言对应翻译成机器可执行的指令

4.2 在Ubuntu下汇编的命令

汇编指令:as hello.s -ohello.o

4.3 可重定位目标elf格式

获得hello.o文件的ELF格式的指令:readelf -a hello.o > hello.elf

  1. ELF头

ELF头以一个十六字节的序列开始,接下来描述了一些详细信息,以帮助链接器进行分析;如数据字节顺序(此处采用小端法)数据表示方法(补码),目标文件类型(REL可重定位文件),系统架构(Advanced Micro Devices X86-64),入口点地址,程序头起点,节头部的起始位置(1668),ELF头的大小(64bit)以及节头部表中条目的大小与数量(大小为64(bytes),数量为13)

2节头:描述hello.o文件中出现的各节的类型,位置和空间大小信息

2重定位节:包含了.text 节需要重定位的信息,在生成可执行文件的时候会修改

如图偏移量是需要被修改的引用的节偏移。标识被修改引用应该指向的符号。类型告知链接器如何修改新的引用(例如类型为R_X86_64_PC32的需要重定位一个使用32位PC相对地址的引用。)。加数是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

该重定位节有八个条码分别是对L0,puts,exit,L1,printf,atio,sleep,getcher的重定位。

 

  1. Symbol节

是一个符号表,它存放着在程序中定义和引用的函数和全局变量的信息。

 

4.4 Hello.o的结果解析

指令:objdump -d -r hello.o>hello.txt

Hello.txt

Hello.s

对比hello.s与hello.txt(总体差别不大,但有一些细节上的差距)

1,跳转指令

Hello.txt 中的跳转指令直接使用确定的地址或者相对寻址进行跳转,而hello.s中的跳转指令使用的是段标识名称

2.立即数

Hello.txt的立即数都采用十六进制,而hello.s中的立即数采用十进制

  1. 函数调用

Hello.s中函数调用的方式是call后直接跟着函数名,而hello.txt中函数的调用方式是call后跟随着一个相对地址(函数与下一条指令的相对位置加上下一条指令的地址)来调用。

  1. 变量的访问地址

例如对第一个printf中的字符串进行访问,hello.s采用的是leaq .LC0(%rip), %rdi,hello.txt采用的是lea    0x0(%rip),%rdi ,

4.5 本章小结

本章简单介绍了汇编的过程,分析了elf的格式和内容,然后利用hello.s与hello.o反汇编形成的hello,txt的对比。简单的分析了一下机器语言和汇编语言的映射关系。

第5章 链接

5.1 链接的概念与作用

链接的概念:链接( linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在现代系统中,链接是由叫做链接器(linker)的程序自动执行的。

链接的作用:链接可以在编译汇编加载是进行,方便了程序的模块化编程。有助于我们更好的管理模块。

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.3 可执行目标文件hello的格式

命令:readelf -a hello > hello1.elf

Hello1.elf类似于hello.elf的格式

  1. ELF头

ELF头描述了文件的基本信息包括程序的数据格式、入口点(即当程序运行时要执行的第一条指令的地址)

2节头

节头格式与hello.elf基本一样,只不过经过重定位后,地址发生了一些变化。

3程序头

4段节

5动态偏移表

 

6重定位节

7符号表

8版本符号节

9版本需求节

5.4 hello的虚拟地址空间

用edb打开hello,可以在Data Dump里看到hello的虚拟地址空间,程序的虚拟地址空间为 0x00000000004000000-0x0000000000401000。

   

查看hello1.elf的程序头部分

 

PHDR:程序头表

INTERP:包含了动态链接过程中所使用的解释器路径和名称。

LOAD:第一个是代码段,第二个是数据段。在程序运行时需要映射到虚拟空间地址。

DYNAMIC:动态链接器所使用的信息

NOTE::辅助信息

GNU_EH_FRAME:保存异常信息

GNU_STACK:使用系统栈所需要的权限信息

GNU_RELRO:保存在重定位之后只读信息的位置

 

5.5 链接的重定位过程分析

命令:objdump -d -r hello>hello1.txt

对比

1链接将程序中调用过得函数的汇编代码也加入到了程序里,hello1.txt中出现了puts getchar,atio,exit,sleep的汇编代码

2链接增加了新的节

Hello.txt文件中增加了_init和.plt节,和一些节中定义的函数

3函数调用,链接后每个函数都有了确定的运行是地址,调用该函数时只需直接跳转到相对应的地址即可。

4地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。我们可以发现,在链接后.text和.plt的相对位置确定,因此在跳转时可以直接通过:目标地址 = PC + 偏移量,来计算出函数地址来进行跳转。

链接过程::链接器ld将各个目标文件组装在一起,就是把.o文件中的各个函数段和数据段按照一定规则累积在一起,并生成可执行文件。

5.6 hello的执行流程

0x400488 init

0x4004a0 .plt

0x4004b0 puts@plt

0x4004c0 printf@plt

0x4004do geychar@plt

0x4004e0 exit@plt

0x4004f0 sleep@plt

0x400500 _start

0x400530 _dl_relocate_static_pie

0x400532 main

0x4005c0 _libc_csu_init

0x400630 _libc_csu_fini

0x400634 fini

 

5.7 Hello的动态链接分

为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略

 

动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放着PLT函数调用指令的下一条指令地址。我们先来看一下GOT表的位置。

do_init调用之前

do_init调用之后

 

5.8 本章小结

本章讲解了hello链接的定义与过程,并将hello和hello.o的反汇编代码进行了比较,大体上推断出了链接的过程。同时,又用edb进行跟踪,来查看动态链接过程中内存中发生的变化。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统的结构的基础

进程的作用:进程创造了一种假象,即程序独占地使用处理器和内存,并且不间断地执行其指令。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。

 

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

壳Shell-bash的作用:shell是一个应用程序,他提供了一个用户界面与系统内核进行交互的平台,壳是一个命令解释器;他接受用户命令,将其解释为计算机可执行的命令,然后将其送入内核,调动相应的程序。

壳的处理流程:

  1. 读取用户输入
  2. 处理输入内容,获得输入参数。
  3. 如果是内置命令则直接执行,否则调用相应程序
  4. 程序运行时shell监视用户输入并响应

 

6.3 Hello的fork进程创建过程

1.在shell中输入相应命令

2.shell分析命令,发现这不是内置命令,此时shell作为父进程通过fork函数创建一个新的进程给hello。

使用fork创建的子进程得到父进程有相同的虚拟地址空间的(但他们是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程可以读写父进程打开的任何文件,父进程和新创建的子进程之间最大的区别在于它们有不同的PID

父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

 

6.4 Hello的execve过程

Fork之后execve函数在当前进程的上下文中加载并运行一个新程序,即可执行目标文件hello,加载完之后,执行以下步骤

1:删除已存在的用户区域

2:映射私有区域,为hello程序的代码,数据,bss和栈区域分配新的区域,这些区域是私有的,都是写时复制。

3:映射共享区域,hello执行时所调用的由共享库创建的函数都是动态链接到hello上的,他们都放在虚拟地址空间的共享区域内。

4:设置程序计数器使之指向代码区域的入口点。

 

6.5 Hello的进程执行

先了解一些基本概念

上下文:进程的物理实体(代码和数据等)和支持进程运行的环境合称为进程的上下文;由进程的程序块、数据块、运行时的堆和用户栈(两者通称为用户堆栈)等组成的用户空间信息被称为用户级上下文。

并发:多个流并发地执行的一般现象被称为并发,一个进程和其他进轮流运行的概念称为多任务。。

时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。

上下文切换

操作系统内核使用一种称为上下文切换(context switch)的较高层形式的异常控制流来实现多任务。上下文切换机制建立在较低层异常机制之上的。

内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。

用户模式和内核模式:处理器为了安全起见,不至于损坏操作系统,必须限制一个应用程序可执行指令能访问的地址空间范围。就发明了两种模式用户模式和内核模式,其中内核模式(上帝模式)有最高的访问权限,甚至可以停止处理器、改变模式位,或者发起一个I/O操作,处理器使用一个寄存器当作模式位,描述当前进程的特权。进程只有当中断、故障或者陷入系统调用时,才会将模式位设置成上帝模式,得到内核访问权限,其他情况下都始终在用户权限中,就能够保证系统的绝对安全。

Hello进程执行:hello一开始运行在用户模式中;直到执行到sleep函数,该函数让hello进行休眠,内核处理器执行上下文切换,进入到内核模式,当定时器到达规定时间(atoi(argv[3])时,发生定时器中断,内核切换会用户模式

 

6.6 hello的异常与信号处理

Linux系统上支持的30种不同类型的异常信号及其相应事件和默认行为。

 

正常操作

  1. 乱按

 

运行时乱按不会影响运行结果,只是但按到回车符时会把乱按时输入的字母当做命令显示在屏幕上

                                                                                                                          

  1. 按Ctrl-Z

运行时按Ctrl-Z后会发送一个SIGTSTP信号,当父进程接受到这个信号后会把hello进程挂起,等待收到特定信号后继续执行

 

  1. 运行ps

显示当前系统中的进程和PID

 

  1. 运行jobs

列出当前shell中已启动的任务的状态

  1. 运行pstree

以树状图显示进程间的关系

 

  1. 运行kill

杀死一个进程,如kill -9 16532 杀死pid为16532的进程

6.7本章小结

本章说明了进程的,主要讲解了shell对指令的处理流程,调用fork创建新进程调用execve加载hello程序,还讲解了hello的异常与信号处理,并对hello做了一系列的测试。
第7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址:源代码经过预处理,编译,汇编后生成的地址,出现在汇编文件中,是由一个段加上一个偏移量组成,偏移量是实际地址与段开始时的地址之间的距离 。

2线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入;

3虚拟地址:在采用虚拟内存的系统中,CPU从一个有2n个地址的地址空间中生成虚拟地址。实际上就是线性地址。

4物理地址:计算机的主存被组织为M个字节大小的连续内存组成的数组,每个字节有唯一的地址,计算机利用地址总线来访问这些地址。

 

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

Intel处理器在通过段式管理寻址时,首先通过段描述符得到段基址,然后与偏移量结合得到线性地址,从而得到了虚拟地址。至于偏移量,基址寄存器还是变址寄存器有不同的计算方法,后者需要经过乘比例因子等处理。

    一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:

最后两位涉及权限检查,这里不予以论述。 索引号,是“段描述符(segment descriptor)”,具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成,如下图:

图示比较复杂,可以利用一个数据结构来定义它,不过,在此只关心一样,就是Base字段,它描述了一个段的开始位置的线性地址。Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。什么时候该用GDT,什么时候该用LDT是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],

1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

3、把Base + offset,就是要转换的线性地址了。 

 

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

分页:线性地址(书里的虚拟地址VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。

虚拟页存的三种状态:未分配的、缓存的、未缓存

从虚拟地址到物理地址的映射通过MMU来实现,如果已缓存则命中,否则称为缺页,发生缺页时MMU会选择一个牺牲页,在物理内存将之前缺页的虚拟内存对应的数据复制到它的位置,并更新页表,然后重新触发虚拟地址翻译事件。

通过页表,MMU可以实现从虚拟地址到物理地址的映射。

 

 

使用页表的地址翻译

Cpu中的页表基址寄存器指向当前页表,n为的虚拟地址分为两部分:一个排位的虚拟页面偏移(vpo)和一个n-p位的虚拟页号(VPN)

MMU利用VPN选择适当的PTE,然后将页表条目中的物理页号(PPN)与虚拟地址中的VPO串联起来,得到物理地址。

 

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

CPU每产生一个虚拟地址,MMU就必须查阅相应的PTE,这显然造成了巨大的时间开销。为了减少这种开销,MMU中有一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)

如下图

首先CPU产生一个虚拟地址,。若TLB命中,则MMU从TLB中取出相应的PTE,最后,MMU将这个虚拟地址翻译成一个物理地址。

当TLB不命中时,MMU从L1缓存中取出相应的PTE。新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。

使用四级页表层次结构时,虚拟地址被划分为 4 个 VPN 和 1 个 VPO。每个 VPNi 都是一个到第 i 级页表的索引,第 j 级页表中的每个 PTE 都指向第 j+1 级某个页表的基址,第四级页表中的每个 PTE 包含某个物理页面的 PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定 PPN 之前,MMU 必须访问四个PTE。

 

 

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

查找数据

在上一步中我们已经获得了物理地址 VA,如图 7.9,使用 CI(后六位再后六 位)进行组索引,每组 8 路,对 8 路的块分别匹配 CT(前 40 位)如果匹配成功且块的 valid 标志位为 1,则命中(hit),根据数据偏移量 CO(后六位)取出数 据返回。  如果没有匹配成功或者匹配成功但是标志位是 1,则不命中(miss),向下一 级缓存中查询数据(L2 Cache->L3 Cache->主存)。

放置数据

查询到数据之后,如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换

 

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用的时候,内核会为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进场创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本,它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记位私有的写时复制。

当fork函数在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的概念。

 

7.7 hello进程execve时的内存映射

Execve在当前进程中加载并运行新进程是执行以下步骤:

  1. 1删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  1. 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
  2. 映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。

4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。

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

DRAM 缓存不命中称为缺页,即在MMU中查找页表时发现与该地址相对应的物理地址不在内存中。缺页导致页面出错,产生缺页异常,内核调用缺页处理程序。

缺页中断处理:

1判断虚拟地址是否合法,缺页处理程序会搜索区域结构的链表,把该地址和每个区域结构中的vm_start和vm_end做比较。如果指令不合法则触发段错误,从而终止该进程。

2然后检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。

3以上两步检查都没有错误的话,内存会确定一个牺牲页,若页面被修改,则换出到磁盘,再将新的目标页替换牺牲页写入,缺页处理程序返回到原来的进程,重启导致缺页的指令。

 

 

 

7.9动态存储分配管理本章主要讲了hello的存储地址空间,inter的段式管理及hello的页式管理。还讲了intel Core7 在指定环境下介绍了 VA 到 PA 的变换、物理内存访问, 以及 hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

 

 

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break"),它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虛拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种:显式分配器和隐式分配器

显式分配器:要求应用显式地释放任何已分配的块。 

隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块, 自动释放未使用的已经分配的块的过程叫做垃圾收集。

对于带边界标签的隐式空闲链表分配器,一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。

头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。

我们将对组织为一个连续的已分配块和空闲块的序列,这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。注意:此时我们需要某种特殊标记的结束块,可以是一个设置了已分配位而大小为零的终止头部。

Knuth提出了一种边界标记技术,允许在常数时间内进行对前面快的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。

显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。

一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。

 

 

7.10本章小结

本章主要讲了hello的存储地址空间,inter的段式管理及hello的页式管理。还讲了intel Core7 在指定环境下介绍了 VA 到 PA 的变换、物理内存访问, 以及 hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。加深了我们对进程运行时的内存分配机制的理解,让我们对计算机的内存管理机制有了一定的认识。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:所有的IO设备都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。

设备管理:这种设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式的来执行。

 

8.2 简述Unix IO接口及其函数

1.打开文件:

函数:int open(char *filename, int flags, mode_t mode);

Filename被打开的文件名,

Flags打开方式

Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

  1. 关闭文件:

函数:int close(int fd);

内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Fd文件描述符

函数返回值为0则成功-1则失败

  1. 读取文件:

函数:ssize_t read(int fd, void *buf, size_t n);

 fd:文件描述词

buf:缓冲区,即读取的数据会被放到这个缓冲区中去,

 

  1. 写入文件:

函数: ssize_t write(int fd, const void *buf,size_t);

write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置。

返回值为-1则失败,返回值为写入的长度则成功

  1. 改变文件位置:

函数:lseek()

 

8.3 printf的实现分析

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;

}

Printf调用了vsprintf和write两个函数

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); //返回最后生成的字符串的长度

}

观察可知,它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。Vsprintf输出到系统函数中,在Linux下,write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。

 

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。

getchar函数通过调用read函数返回字符。其中read函数的第一个参数是描述符,0代表标准输入。第二个参数输入内容的指针,也就是字符的地址,最后一个参数是1,代表读入一个字符,符号getchar函数读一个字符的设定。read函数的返回值是读入的字符数,如果为1说明读入成功,那么直接返回字符,否则说明读到了buf的最后。

 

8.5本章小结

本章主要介绍了Linux的设备管理方法,简单的了解了一下Unix IO接口以及其函数,并分析了printf函数和getchar函数的具体实现。

 

结论

回顾hello的一生,从他诞生到结束,经历过很多坎坷。让我们来回顾一下hello所经历过得大事记吧!

1预处理,经过预处理生成了hello.i,插入了一些头文件。

2编译:通过编译器生成hello.s文件,为下一步的 汇编奠定了基础。

3汇编:hello.s通过汇编器被汇编成可重定位目标文件hello.o,hello.o是二进制文件

4链接:链接器将hello.o和其他外部文件一起链接,完成相应节的合并与符号的解析,确定程序运行时函数的真实地址,生成可执行文件hello

执行命令下达后,shell利用fork函数为他创建了进程,execve把他加载到新进程中,硬件听从他的调配,他从内存中被读取出来,经过虚拟内存映射、CPU执行指令、内核调度、缓存加载数据、信号处理等一系列 过程最终被输出到显示器上。

他死后,他的父进程shell将其回收。

感悟:计算机上任何一个简单的程序,都需要软硬件协作经过一系列复查的过程才能完成,深入理解计算机系统,有助于我们了解这一过程,对我们编写面向计算机友好的系统有很大帮助。

 

附件

Hello.c 源程序

Hello.i 预处理文本

Hello.s编译得到的文本程序

Hello.o 汇编得到的二进制文本程序

Hello 链接生成的可执行二进制文件

Hello.txt   hello.o文件objdump反汇编生成的汇编文本程序

Hello1.txt   hello文件的objdump反汇编生成的汇编文本程序

Hello.elf   hello.o文件的elf格式。

Hello1.elf  hello.o文件的elf格式。hello.o文件的elf格式。hello.o文件的elf

参考文献

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值