程序人生——hello的一生

 

 

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业          计算机类        

学     号         1190201303       

班     级          1903006         

学       生           王艺丹       

指 导 教 师            史先俊        

计算机科学与技术学院

2021年5月

摘  要

本报告结合本学期计算机系统课程所学知识,详细地阐述了hello程序精彩的“一生”。从其预处理、编译、汇编、链接、进程管理、存储管理与IO管理等方面分别对其“各阶段人生”进展进行了系统地分析。主要为对CSAPP所学知识的梳理,加深对计算机系统的了解。

关键词:计算机系统;hello的一生;预处理;编译;汇编;链接;进程管理;存储管理与IO管理                           

 

 

 

 

 

 

 

目  录

 

 

第1章 概述

1.1 Hello简介

P2P(From Program to Process):

Hello程序是通过code:Blocks等工具编写得到的program,之后经过预处理,编译,汇编,链接,生成可执行文件hello,若用./Hello运行之,则shell会解析命令行参数,并初始化环境变量等,然后调用fork函数创建进程、execve函数运行函数,通过内存映射、分配空间等让Hello与其他进程并发进行。这个从程序到进程的过程即为P2P

020(From Zero-0 to Zero-0):

程序从无开始,在通过P2P中的一系列操作后拥有了自己的进程、内存中地址和时间周期等。而在进程完成终止之后,又会被回收释放内存并删除有关上下文。这个从无到有复归于无的过程就是020

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2GHz;2G RAM;512GB SSD

1.2.2 软件环境

Windows10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位

1.2.3 开发工具

CodeBlocks 64位;gcc等;Microsoft word

1.3 中间结果

中间结果文件名称

作用

hello.c 

源程序文件(c程序文件)

hello.i 

预处理后生成的文本文件

hello.s 

编译后生成的汇编语言文件

hello.o 

汇编后生成的可重定位文件

hello 

链接后生成的可执行程序                                                

hello.elf

hello.o的ELF格式文件

hello_.elf

hello的ELF格式文件

hello_obj

hello.o的反汇编文件

hello_objdump

hello的反汇编文件

1.4 本章小结

本章对hello进行了一个总体的概括,介绍了hello程序运行的大致过程(P2P与020)的意义和过程,介绍了作业中的硬件环境、软件环境和开发工具,最后简述了从.c文件到可执行文件中间经历的过程。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:

预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序并得到以.i为文件扩展名的新的C程序,例如宏定义,文件包含,条件编译等。

1.将所有的#define删除,并展开所有的宏定义;

2.处理所有的预编译指令,例如:#if,#elif,#else,#endif;

3.处理#include预编译指令,将被包含的文件插入到预编译指令的位置;

4.添加行号信息文件名信息,便于调试;

5.删除所有的注释:// /**/;

6.保留所有的#pragma编译指令,因为在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作。

7.生成.i文件。

作用

1.复制引用的文件,例如#include 命令告诉预处理器读取头文件stdio.h的内容,并把它直接插入程序文本中。

2.替换宏定义在源代码中的所有显示。

3.规定限制编译某段代码的条件。

2.2在Ubuntu下预处理的命令

命令:gcc -E -o hello.i hello.c

结果:

生成了hello.i文件

2.3 Hello的预处理结果解析

1.首先预处理程序将#include 、#include 、#include 三个文件的源码内容添加到预处理文件中;

2.后面的一部分则直接复制源文件的内容。

3.如果遇到头文件中包含预处理命令,则递归地将内容复制进去,直到预处理文件中不包含任何预处理命令为止。

预处理方便了编译器将程序翻译成汇编语言时的操作。

2.4 本章小结

本章介绍了预处理的概念、作用,以及在ubuntu下使用预处理的命令。也通过Hello的例子解析了预处理结果。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:

编译是指将一个经过预处理的高级语言程序文本(.i文件)翻译成能执行相同操作的等价ASII码形式汇编语言文件(.s文件)的过程。

编译包括以下基本流程:

1.       语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为自上而下分析和自下而上两种

2.       中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码

3.       代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码

4.       目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码

作用:

将高级语言转换成更接近机器语言的汇编语言,简化将高级语言转换成计算机可执行的二进制文件时的操作。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

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

结果:

生成了hello.s文件

3.3 Hello的编译结果解析

3.3.1 对数据变量的处理

全局变量sleepsecs

它的初始化不需要汇编语句,而是直接完成的

sleepsecs被声明为全局变量且已赋初值,编译器处理时在.data节声明该变量,(.data节存放已经初始化的全局和静态C变量)。

局部变量i

局部变量存储在寄存器或栈中

i是main函数中的局部变量,通过对源程序代码及.s文件的分析可知其存储在栈中地址为-4(%rbp)的空间上,程序运行时才对其进行赋值。

字符串常量:

字符串常量存储在.rodata节中,其中中文字符以\三位数的方式存储

int argc

传入main函数的参数,存储在%edi中,在栈中使用

int main

定义的主函数,存储在.text节中

char* argv[]

传入main函数的参数,存储在%rsi中,在程序运行时被放入栈中使用

数字常量:

4、0、8、1、2、3存储在.text节

3.3.2 算术操作

在循环操作中,使用了自加++操作符:

在每次循环执行的内容结束后,对i进行一次自加,栈上存储变量i的值加1

3.3.3 关系操作和控制转移

程序第14行中判断传入参数argc是否等于4,汇编代码为:

je用于判断指令cmpl产生的条件码,若比较结果不相等则跳转到.L2;

对于for循环中的循环执行条件(i = 0; i < 8; i++)汇编代码为:

jle用于判断指令cmpl产生的条件码,若-4(%rbp)的值小于等于7则跳转到.L4;

3.3.4数组/指针/结构操作

主函数main的参数中有指针数组char *argv[]

在argv数组中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串。

因为char* 数据类型占8个字节,根据

对比原函数可知通过%rsi-8和%rax-16,分别得到argv[1]和argv[2]两个字符串

3.3.5函数操作

X86-64中,过程调用传递参数规则:第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。

 

main函数:

参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。

函数调用:被系统启动函数调用。

函数返回:设置%eax为0并且返回,对应return 0 。

汇编代码如下:

可见argc存储在%edi中,argv存储在%rsi中;

printf函数

参数传递:call puts时只传入了字符串参数首地址;for循环中call  printf时传入了 argv[1]和argc[2]的地址。

函数调用:if判断满足条件后调用,与for循环中被调用。

源代码如下:

汇编代码如下:

exit函数:

参数传递:传入的参数为1,再执行退出命令

函数调用:if判断条件满足后被调用.

汇编代码如下:

sleep函数:

参数传递:传入参数atoi(argv[3]),

函数调用:for循环下被调用,call sleep

汇编代码如下:

getchar函数:

函数调用:在main中被调用,call getchar

汇编代码如下:

3.4 本章小结

本章主要介绍了编译的概念以及过程。同时通过示例函数表现了c语言如何转换成为汇编代码。介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环。编译程序所做的工作,就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码表示。包括之前对编译的结果进行解析,都令我更深刻地理解了C语言的数据与操作,对C语言翻译成汇编语言有了更好的掌握。因为汇编语言的通用性,这也相当于掌握了语言间的一些共性。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中的过程成为汇编

作用

将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

命令:

gcc -c hello.s -o hello.o

结果:

4.3 可重定位目标elf格式

先用readelf命令:

readelf -h hello.o

ELF头:从一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助连接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

再用readelf -a hello.o > hello1elf.txt看elf格式:

节头部表:记录了每个节的名称、类型、属性(读写权限)、在ELF文件中占的度、对齐方式和偏移量。

.text节:以编译的机器代码。

.rela.text节:一个.text节中位置的列表。

.data节:已初始化的静态和全局C变量。

.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。

.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。

.comment节:包含版本控制信息。

.note节:注释节详细描述。

.eh_frame节:处理异常。

.rela.eh_frame节:一个.eh_frame节中位置的列表。

.shstrtab节:该区域包含节区名称。 .symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。

.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。

.symtab节:本节用于存放符号表。

重定位节:表述了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。

本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号

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

最后一部分则是版本等信息

4.4 Hello.o的结果解析

命令:

objdump -d -r hello.o > dishello.s

结果:

分析hello.o的反汇编,并与第3章的 hello.s进行对照分析:

1.       数的表示:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。

2.       分支转移:跳转语句之后,hello.s中是.L2和.LC1等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,也即间接地址。

3.       函数调用:hello.s中,call指令使用的是函数名称,而反汇编代码中call指令使用的是main函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

4.5 本章小结

本章对汇编结果进行了详尽的介绍。经过汇编器的操作,汇编语言转化为机器语言,hello.o可重定位目标文件的生成为后面的链接做了准备。通过对比hello.s和hello.o反汇编代码的区别,令人更深刻地理解了汇编语言到机器语言实现地转变,和这过程中为链接做出的准备,对可重定位目标elf格式进行了详细的分析,侧重点在重定位项目上。同时对hello.o文件进行反汇编,将Disas_hello.s与之前生成的hello.s文件进行了对比。使得我们对该内容有了更加深入地理解。

(第41分)

第5章 链接

5.1 链接的概念与作用

概念:

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行

作用:

把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件

注意:这儿的链接是指从 hello.o 到hello生成过程。

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

结果:

ELF文件头:

节头:

描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

5.4 hello的虚拟地址空间

使用edb加载hello, Data Dump 窗口可以查看加载到虚拟地址中的 hello 程序。查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。在下面可以看出,程序包含PHDR,INTERP,LOAD ,DYNAMIC,NOTE ,GNU_STACK,GNU_RELRO几个部分,如下图所示

其中PHDR 保存程序头表。INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据、程序的目标代码等。DYNAMIC 保存了由动态链接器使用的信息。NOTE 保存辅助信息。GNU_STACK:权限标志,用于标志栈是否是可执行。GNU_RELRO:指定在重定位结束之后哪些内存区域是需要设置只读。

5.5 链接的重定位过程分析

命令:

objdump -d -r hello > hello_objdump.s

结果:

分析hello与hello.o的不同:

1.链接增加新的函数:

在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。

2.增加的节:

hello中增加了.init和.plt节,和一些节中定义的函数。

3.函数调用:

hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

4.地址访问:

hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

链接的过程:

根据hello和hello.o的不同,分析出链接的过程为:

链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。

5.6 hello的执行流程

子函数名和地址(后6位)

401000 <_init>

401020 <.plt>

401030

401040

401050

401060

401070

401080

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>

调试edb找到call语句:

5.7 Hello的动态链接分析

在elf文件中可以找到:

进入edb调试:

对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域

5.8 本章小结

章主要了解温习了在linux中链接的过程。通过查看hello的虚拟地址空间,并且对比hello与hello.o的反汇编代码,更好地掌握了链接与之中重定位的过程。不过,链接远不止本章所涉及的这么简单,就像是hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

概念:

进程的经典定义就是一个执行中程序的实例。在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行 我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。

作用:

每次运行程序时,shell创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。

进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。

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

作用:

它接收用户命令,然后调用相应的应用程序。是命令解析器。

处理流程:

1.       用户输入命令

2.       shell对用户输入命令进行解析,判断是否为内置命令若为内置命令

3.       是则调用内置命令处理函数

4.       不是则在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。

6.3 Hello的fork进程创建过程

根据shell的处理流程,可以推断,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

6.4 Hello的execve过程

execve函数加载并运行可执行文件hello,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。运行时,创建一个内存映像,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,接下来,加载器跳转到程序的入口,_start函数的地址,这个函数是在系统目标文件ctrl.o中定义的,对所有的c程序都一样。_start函数调用系统启动函数,_libc_start_main,该函数定义在libc.so里,初始化环境,调用用户层的main函数,处理main函数返回值,并且在需要的时候返回给内核。

结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello:

1.     删除已存在的用户区域(自父进程独立)。

2.     映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。

3.     映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。

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

6.5 Hello的进程执行

逻辑控制流:

一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。

用户模式和内核模式:

处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

上下文:

上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。

示例:sleep进程的调度过程

初始时,控制流再hello内,处于用户模式

调用系统函数sleep后,进入内核态,此时间片停止。

2s后,发送中断信号,转回用户模式,继续执行指令。

调度的过程:

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

以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。

用户态与核心态转换:

为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创世模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理

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

 正常运行:

乱按:

会把回车之前的字符串当作命令,在hello结束之后尝试运行

Ctrl+C

会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程,通过ps命令发现这时hello进程已经被回收。

Ctrl+Z

挂起进程,可以执行jobs等指令,在输入fg后继续执行

包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.7本章小结

本章了解了hello进程的执行过程。主要讲hello的创建、加载和终止,通过键盘输入。程序是指令、数据及其组织形式的描述,进程是程序的实体。可以说,进程是运行的程序。在hello运行过程中,内核有选择对其进行管理,决定何时进行上下文切换。

同样是在hello的运行过程中,当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对不同的异常信号,hello也有不同的处理结果。

对hello执行过程中产生信号和信号的处理过程有了更多的认识,对使用linux调试运行程序也有了更多的新得。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:

在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。即hello中的偏移地址。

线性地址:

是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址:

虚拟地址是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为"段:偏移量"的形式,这里的段是指段选择器。就是hello的虚拟内存。

物理地址:

是出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。是hello的实际地址。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
    索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
    这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。
    全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
    GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
   给定一个完整的逻辑地址段选择符+段内偏移地址,
   看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
   拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

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

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

VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块,类似的,物理内存被分割为物理页。系统通过操作系统软件、mmu中的地址翻译硬件和一个存放在物理内存中叫做页表的数据结构来完成缓存各种操作。页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。

页表就是一个页表条目的数组。虚拟地址空间中的每个页在页表中一个固定的偏移量处都有一个PTE。PTE包含了一个有效位和一个n位字段,有效位表明了该虚拟页当前是否被缓存在DRAM中。因为DRAM是全相联的,所以任意物理页都可包含任意虚拟页。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个n-p位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE.将页表条目中的物理页号和VPO串联起来就是相应的物理地址。因为物理和虚拟页面都是P字节的,所以物理页面偏移和虚拟页面偏移是相同的。

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

多级页表:

将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。

解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA

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

CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CS(组号),CO(偏移量)。根据CS寻找到正确的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一得PID。同时为这个新进程创建虚拟内存。

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

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

1、删除已存在得用户区域;

2、映射私有区域;

3、映射共享区域;

4、设置程序计数器。

execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点,下一次调度这个进程时,他将从这个入口点开始执行。

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

DRAM缓存不命中称为缺页。假设某时刻CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发了一个缺页异常。缺页异常调用内核中的缺页异常处理程序。缺页异常处理程序会选择一个牺牲页,比如存放在PP3中的VP4。如果VP4已经被修改了,那么内核会将它复制回磁盘。内核会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,然后返回。当异常处理程序返回时,它会重启导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块(block)的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式的保留为供应程序使用。空闲块保持空闲,直到它被应用所分配。已分配的块保持已分配,直到它被释放。

显示分配器要求应用显示地释放任何已分配的块。隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个快。

malloc函数返回一个指针,指向大小为至少size字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。若malloc遇到问题,就返回NULL,并设置errno。malloc不初始化它返回的内存。

隐式空闲链表:

空闲块通过头部中的大小字段隐含的链接着,分配器可以可以通过遍历堆中所有的块,从而间接的遍历整个空闲块的集合。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个大小足够放置所请求块的空闲块。分配器执行这种搜索的方式是由放置策略确定的。一些常见的策略是首次适配、下一次适配和最佳适配。

一旦分配器找到一个匹配的块,他就必须做另外一个决定,那就是分配这个空闲块中多少的空间。

若找不到合适的空闲块,一个选择是合并那些在内存中物理上相邻的空闲块来创建一些更大的空闲块。,如果还不够,就会通过sbrk函数,向内核请求额外的堆内存。至于合并策略,分配器可以选择立即合并或推迟合并。

带边界标记的合并:

在每个块的结尾处添加一个脚部(头部的一个副本),分配器可以通过检查他的脚部,判断前一个块的起始位置和状态。这个脚部总是在距离当前快开始位置一个字的距离。

显式空闲链表:

将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。

分离的空闲链表:维护多个空闲链表,其中每个链表中的块有大致相等的大小。以下是几种基本的分离存储方法:

1、简单分离存储:每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。

2、分离适配:分配器维护着一个空闲链表的数组。每个空闲链表是和一个大小类相关联的,并被组织成一种类型的显式或隐式链表。每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。伙伴系统是分离适配的一种特例,其中每个大小类都是2的幂

7.10本章小结

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

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

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

打开文件:

 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  Linux shell创建的每个进程开始时都有三个打开的文件: 标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。
  改变当前的文件位置: 对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。

读写文件:

一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似的,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

 

函数:

1、进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的:int open(char *filename, int flags, mode_t mode)。若成功返回新文件描述符,若出错则返回-1.open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。最后进程通过调用close函数关闭一个打开的文件。

2、通过read和write函数进行输入与输出:ssize_t read(int fd, void *buf, size_t n)若成功返回读的字节数,若EOF则为零,出错为-1;ssize_t write(int fd, const void *buf, size_t n)成功为写的字节数,出错为-1.read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。也可以通过RIO包处理不足值。它提供了两类不同的函数:无缓冲的输入输出函数;带缓冲的输入函数。

3、应用程序通过调用stat和fstat函数检索关于文件的信息。Int stat(const char*filename,struct stat *buf);int fstat(int fd,struct stat *buf);若成功返回0,出错返回-1.stat函数以一个文件名作为输入,并填写stat数据结构中的各个成员。fstat相似,只不过是以文件描述符而不是文件名作为输入。stat数据结构中的st_mode编码了文件访问许可位和文件类型。St_size则包含了文件的字节数大小。

4、通过readdir系列函数来读取目录的内容。DIR*opendir(const char *name)若成功返回处理的指针,若出错,返回NULL,它以路径名为参数,返回指向目录流的指针。Struct dirent *readdir(DTR *dirp)若成功返回指向下一个目录的指针,若没有更多目录或出错则返回NULL。每次对readdir的调用返回都是指向流dirp中下一个目录项的指针,或者没有更多目录项则返回NULL.每个目录项都是一个结构。若出错,readdir返回NULL并设置errno。函数closedir关闭流并释放所有找资源。成功返回0,失败返回-1.

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接受一个格式化的命令,并把指定的匹配的参数格式化输出。而在printf中用了vsprintf和write两个函数。

printf函数引用的vsprintf函数:

int vsprintf(char *buf, const char *fmt, va_list args)

{

    char *p;

    chartmp[256];

    va_listp_next_arg = args;

    for (p = buf; *fmt; fmt++)

    {

        if (*fmt != '%')

        {

            *p++ = *fmt;

            continue;

        }

        fmt++;

        switch (*fmt)

        {

        case 'x':

            itoa(tmp, *((int *)p_next_arg));

            strcpy(p, tmp);

            p_next_arg += 4;

            p += strlen(tmp);

            break;

        case 's':

            break;

        default:

            break;

        }

        return (p - buf);

    }

}

vsprintf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar原函数:

int getchar(void)

{

    static char buf[BUFSIZ];

    static char* bb=buf;

    static int n=0;

    if(n==0)

    {

        n=read(0,buf,BUFSIZ);

        bb=buf;

    }

    return (--n>=0)?(unsigned char)*bb++:EOF;

}

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要阐述了Linux的IO设备管理办法以及IO接口实现与相应的函数实现。分析了getchar()和printf()函数的实现。

(第81分)

结论

Hello从被程序员在编程软件编写代码的那一刻起,慢慢成长为一个合格的程序,而后,又在计算机系统内经历了精彩丰富的一生:

1.   hello.c经过预处理,扩展得到hello.i文本文件

2.   hello.i经过编译,得到汇编代码hello.s文本文件

3.   hello.s经过汇编,得到二进制可重定位目标文件hello.o

4.   hello.o经过链接,生成了一个完全链接的,可以加载运行可执行文件hello

5.   fork函数创建子进程;execve函数加载并运行程序hello

6.   hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址。

7.   通过内存管理、信号处理共同建立了相应的时间与空间,用I/O管理输入输出,度过精彩的人生

8.   hello最终被shell父进程回收,内核回收为其创建的所有信息,hello的一生也随之结束了

CSAPP贵为计算机基础书籍顶级之作,介绍了计算机系统的基本概念,包括最底层的内存中的数据表示、流水线指令的构成、虚拟存储器、编译系统、动态加载库,以及用户应用等。书中提供了大量实际操作,可以帮助读者更好地理解程序执行的方式,改进程序的执行效率。此书以程序员的视角全面讲解了计算机系统,深入浅出地介绍了处理器、编译器、操作系统和网络环境。不愧是是这一领域的权威之作!

作为一名计算机学院的HITer,我们的学习是实践理论相结合的,深入理解计算机系统对我们成为一名优秀的计算机专业相关人才意义重大。希望我们在我们的学习生涯中,可以同hello的一生一样,精彩且丰富!

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

附件

文件的作用

文件名

预处理后的文件

hello.i

编译之后的汇编文件

hello.s

汇编之后的可重定位目标文件

hello.o

链接之后的可执行目标文件

hello

Hello.o 的 ELF 格式

helloelf.txt

Hello.o 的反汇编代码

dis_hello.s

hello的ELF 格式

hello.elf

hello 的反汇编代码

hello_objdump.s

列出所有的中间产物的文件名,并予以说明起作用。

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

​参考文献

为完成本次大作业你翻阅的书籍与网站等

[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
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值