程序人生 hello.c

摘  要

关键词:预处理, 编译, 汇编, 链接, 进程, 虚拟内存,IO管理

当我们写下hello.c时,我们真的知道我们在做什么吗?

本文的目的就是深入浅出的探讨hello.c这样一个简单的程序究竟时如何被运行的,对它的出生到结果有一个完整的认识。

hello在成为可以被机器读取的文件前经历了什么?有几个阶段?各自有什么作用?hello是如何被机器读取并一步步在计算机中执行的?hello的内容如何在计算机中被保存?我们的输入是如何被hello使用,hello又是如何让我们想要的输出出现在我们的屏幕上的?

这些都是我们没有想过的问题,也是本文的主要内容。

本文将通过实验与查阅资料的方法,给上面的问题做了粗略的解答。为仅仅只写代码的入门程序员打开了新的角度来看待自己写的程序,也揭示了硬件对程序运行的巨大影响。

了解了这部分的内容,可以帮助我们写出更好的程序,更适合被计算机执行的程序,这是代码层面的修改无法企及的。

                           

目  录

1 概述................................................................................................................ - 4 -

1.1 Hello简介......................................................................................................... - 4 -

1.2 环境与工具........................................................................................................ - 4 -

1.3 中间结果............................................................................................................ - 4 -

1.4 本章小结............................................................................................................ - 4 -

2 预处理............................................................................................................ - 5 -

2.1 预处理的概念与作用........................................................................................ - 5 -

2.2Ubuntu下预处理的命令............................................................................. - 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 Hellofork进程创建过程........................................................................ - 10 -

6.4 Helloexecve过程.................................................................................... - 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与四级页表支持下的VAPA的变换................................................ - 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 helloIO管理.................................................................................... - 13 -

8.1 LinuxIO设备管理方法............................................................................. - 13 -

8.2 简述Unix IO接口及其函数.......................................................................... - 13 -

8.3 printf的实现分析........................................................................................... - 13 -

8.4 getchar的实现分析....................................................................................... - 13 -

8.5本章小结.......................................................................................................... - 13 -

结论............................................................................................................................ - 14 -

附件............................................................................................................................ - 15 -

参考文献.................................................................................................................... - 16 -

第1章 概述

1.1 Hello简介

 

                                  1.1 helloP2P的过程

我们的程序先经过预处理,通过编译器将其转化为汇编程序,在经过汇编器对汇编程序的修改,转化为单纯的二进制的,机器语言的数字码,在经过链接器,将代码组合后,就成了我们机器可执行的最终文件。

020的过程,就是程序从无到有,再到无的过程。这部分要解释的是,我们的程序是如何被调用进计算机成为一个运行的进程的,又是如何在执行完毕后从计算机中消失的。

P2P的过程中,当shell接受用户的执行请求,将我们的程序调用出来成为进程。系统会为程序分配虚拟内存,将程序运行所需要的上下文存放,之后,当shell通过execve将程序加载,并将虚拟内存放入物理内存中供使用。这时,其会通过fork为程序创建一个新的进程,程序就在这个进程中开始运行了。CPU会为其设置时间片,控制其逻辑流。在程序执行完毕后,创建这个子进程的父进程会将这个进程回收,于是这个进程就消失了。

1.2 环境与工具  

1.2.1 硬件环境    

X64;2.60GHz;16G RAM;952GB

1.2.2 软件环境

Windows11;VMware11;Ubuntu 16.04;

1.2.3 开发工具

VisualStudio 2017;Codeblocks; VIM - Vi IMproved 8.1;

gcc (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0

1.3 中间结果

             

 

                                                        1.3 中间结果展示

          hello.c是我们编写的代码的源程序,也就是程序的源头;hello.i是经过cpp

       预处理后的文件;hello.s是经过编译器编译过后的文件,这时仍是文本文件;

       hello.o是经过汇编后的可重定位文件,此时其已经转化为二进制文件;最后是

       hello.out最后生成的可执行文件;hello.txt是运行反汇编的输出内容。

        

1.4 本章小结

              本章主要介绍了程序执行的基本过程,其如何从程序变成进程,其如何产

       生和消失,并附带了作业过程的软硬件环境,以及所需用到的中间文件介绍。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理是在编译之前对源代码进行简单处理的过程,使其便于下一步工作

       其主要处理了#开头的一些内容,修改原本的程序生成hello.i进行编译。

  作用:1、处理文件包含:对于C源程序中的#include指令,其会读取其中内容并把它们直接插入到程序文本中

2处理宏定义:对于#define指令,用所定义的实际值去替换代表它的符号

3处理注释:删除C源程序中的被规定视为注释的内容

4处理条件编译:根据条件编译指令(如#if#elif#else等),按条件选择符合的代码送至编译器编译

       5、处理一些特殊的控制指令

2.2在Ubuntu下预处理的命令

 

                                         2.2 Ubuntu下预处理的命令

cpp进行预处理。

2.3 Hello的预处理结果解析

附上源文件全文以便于对比。

 

 

2.3.1 源程序内容                                2.3.2 hello.imain

右侧说明,预处理过后,main函数里的部分没有发生改变。

但其他部分有所改变,接下来依次进行说明:

1、所有的注释都被去除了;

2、所有的include文件集都被读取了,其内容都被输入到hello.i中;

图2.3.5对stblib的读取


2.3.3 stdio.h的读取                                2.3.4 unistd.h的读取

              但是我们可以观察到,对头文件的读取除了我们自身在源程序中调用的这

       些文件以外,还额外打开了别的头文件。这是由于我们所调用的头文件下还有

       另外的头文件需要被打开并引入文件。

              在每个文件读取完的下方,我们都可以观察到大片的代码——不是由我们     自己编写的代码,这些代码就是cpp在读取头文件后,将其中的内容全部加入

       到我们的原本的代码之中,这也是为什么我们短小的代码预处理之后变得非常

       长的原因。

2.4 本章小结

本章对预处理进行了讲解。首先给出了预处理的方法,接着我们查看了预处理后的输出文件,与源文件进行了比对,展现了预处理的作用。

第3章 编译

3.1 编译的概念与作用

概念:从预处理后的文件到汇编语言程序的过程;

作用:编译的流程与作用主要有以下几个部分:

        1、语法分析:分析我们的输入文件,判断每个单词是否为一个合法的

               语法单元;

        2、生成中间代码:生成便于优化,便于解释的中间代码

        3、代码优化:从中间代码出发,对原本的代码进行有限但合理的优化,

               使其执行更有效,但不改变代码原本的意思;

        4、生成最终代码:在上述流程完成之后,其将我们预处理后的文件转化

              为汇编语言,便于转化为二进制代码。       

3.2 在Ubuntu下编译的命令

3.2Ubuntu下编译的命令

通过gcc -S 对源文件进行编译

3.3 Hello的编译结果解析

3.3.1代码解构分析

      这部分是前提工作,在分析了生成的代码结构之后,我们才能对其进行分

      类与解构。

     

                                         3.3.1文件声明

      main 函数之前的部分,其对执行之前的工作进行了说明。

      .file 说明了源文件名 “hello.c”.text是代码段,.section按指示将代码划分

.rodata说明是只读数据,.align说明了对齐方法,.string指的是字符串,.global

是全局变量,本程序只有一个main,.type指定类型,main的类型是一个函数

图3.3.2 main函数内容

     

以上内容是main函数的主体内容,我们将在下一部分具体分析其和代码之间的关系

左侧内容是一些编译相关的信息,不是本节内容的重点

3.3.3 函数之后的内容

3.3.2数据类型

              (1)常量:

                     ①输出字符串:

                            被储存在只读数据内,不被改变;

             

3.3.4 字符串常量

             ②立即数:

文件中有若干’$’开头的立即数

3.3.5 立即数常量(仅举两个例子)

      (2)变量

             ①全局变量:

                    仅有全局属性的函数main

             ②局部变量:

从源程序上看,应有局部变量int i;这个变量被用于循环条件判断

图3.3.6 文件截图

                   

其与一个立即数进行比对,否则跳转到L4,其中L4L3前,可

             以判断这就是循环判断,那么我们就知道i存放在%rbp寄存器相关的

             地址上,所以其存放在栈上,那么其何时被创建?

图3.3.7文件截图

                   

结合之前的分析,i在这一步中被创建。

      (3)赋值

(2)中我们知道系统给变量i,一个我们没有初始定义的变量赋了0,这是在循环的语句中执行的。

      (4)算术操作

             程序仅涉及一次算术操作:i++;

3.3.8 执行i++的命令

              上面截图中的指令就执行了我们的算术操作。

      (5)关系操作与控制转移

             这两部分合在一起讲,因为在程序的执行中,这两者同时出现

             源程序有一个if和一个循环判断:

3.3.9if的判断语句

             可以知道argv就在图中所示的地方,其与立即数4对比,显然这就是                   if实现的地方。

3.3.10 执行循环的代码块

             上面的代码块展示了循环如何被执行。在L3中,其对i进行判断,符           条件则返回L4,在L4中,又对i+1,实现了循环的计数。

      (6)数组/指针操作

             汇编代码是通过寻址的方式来进行数组的遍历的:

3.3.11数组的创建

函数的参数是一个数组,被存放在栈上。在调用时,我们通过偏移的方式从数组的头部寻址,这里的截图在上面循环的代码块3.3.10是一样的。

可以注意到,在进行调用的时候,代码先取了一次(),意思是先寻了一次地址,这是由于我们输入的参数是一个指针数组导致的。

      (7)函数操作:

             ①参数传入:

                    根据之前的分析可以知道,参数是在栈中传入函数,供其使用

             主要是通过寄存器来实现;

             ②函数调用:

                    函数的调用是通过call指令来实现的

3.3.12 call调用函数

              ③函数返回:

                    函数的返回通过ret命令来返回

                                                

                                                 3.3.13ret进行返回

3.4 本章小结

本章先介绍了编译的内容与方式,之后结合程序编译的实例,逐步分析了源程序和编译代码之间的关联,解释了c代码如何被编译代码实现。

第4章 汇编

4.1 汇编的概念与作用

       概念:将汇编代码文件hello.s转换为机器语言的二进制文件hello.o的过程

      

作用:把汇编语言翻译成机器语言,用二进制码代替汇编语言中的符号,即让它成为机器可以直接识别的程序

4.2 在Ubuntu下汇编的命令

4.2 Ubuntu下的汇编命令

4.3 可重定位目标elf格式

用指令readelfhello.oelf格式内容存放到elf.txt中。

4.3.1 ELF头:

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

4.3.1 elf头的内容

4.3.2 节头部表:

      节头部表描述了hello.o文件中出现的各个节的类型、位置、所占空间大小、

读写权限等内容,其中,表中共有13个节。

4.3.2 节头部表的内容

             其中每个表项的意思如下:

           .text节:已经编译的机器代码

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

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

              .bss节:未初始化的全局和静态C变量,以及所有被初始化为0     全局或静态变量

              .rodata节:存放只读数据

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

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

.eh_frame节:处理异常

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

4.3.3 重定位节表:

            

            

4.3.3 重定位节表

             重定位表提供了链接器在进行链接时所需要的信息。表中一共有9

      表项,有8个在text节中需要被链接,有一个在.eh_frame中需要被链接。

其中,偏移量是被修改链接的内容的字节偏移,信息包含了重定位目标在.symtab中的偏移量和重定位类型,类型指的是其重定位类型,符号名称是被引用的对象的名字,加数作用是在一些重定位中进行偏移调整。

       4.3.4符号表

             

4.3.4符号表

            符号表包含了程序中引用的全局变量和定义的函数的信息

4.4 Hello.o的结果解析

4.4.1 反汇编文件内容

      反汇编文件的内容与hello.s大体相同,但其有几个地方的使用并不相同。

4.4.1操作数的表示

        hello.s中,其用十进制数来表示,在反汇编文件用16进制表示

4.4.2 相同的语句操作数的不同表示

4.4.2代码块

        hello.s中有许多代码块,但是在反汇编文件中没有这样的解构

4.4.3文件介绍

       反汇编文件中并不包含hello.s中一开始对文件介绍的文件

4.4.4 函数的调用

4.4.3 相同的语句对函数调用的不同方式

        hello.s中,函数的调用通过call+函数名的方式来进行调用,而反汇编    文件中其采用了间接寻址的方式来进行调用。其中,函数的相对地址尚未确

  定,因此均用0代替。

4.4.5 逻辑功能

                                  4.4.5 相同的语句对条件跳转的不同表示

        hello.s中,由于有代码块,于是条件控制是通过在代码块间跳转的方

  式来实现的,而在反汇编文件中,其实通过间接寻址的方式来进行跳转的

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结

本章主要介绍了汇编,并分析了汇编文件的内容,并从反汇编的角度比对了汇编之后的影响,汇编真正的开始把程序转变成了机器可以读懂的二进制语言

第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.2 Ubuntu下链接的命令

5.3 可执行目标文件hello的格式

       先用readelfhello读取,并输入到hellof.txt方便阅读。   

       5.3.1 ELF头:

             

5.3.1ELF

              与上一节类似,ELF描述文件总的信息。

       5.3.2节头

5.3.2节头

              这部分描述了hello中出现的所有节的主要信息。包括名称,类型,地址和

偏移量。编译器重新链接时,会把这些节看做一个大块,根据大块大小和偏移量重新分配地址。其中,表项的排序跟地址有关。

      

       5.3.3程序头

5.3.3程序头表

       5.3.4重定位节

             

5.3.4重定位节

       5.3.5符号表

              在文件中的不同部分有各自的符号表,这里不一一列举。

5.4 hello的虚拟地址空间

5.4.1 datadump的截图

       联系5.3中程序头的截图,可以发现其列出的虚拟地址,在图5.4.1中均

可以找到对应的内容。而在第一行0040000处其有标识elf说明程序从这里

被加载。

5.4.3Symbols的截图

      可以观察到,我们在5.3节中看到的节头虚拟地址在图5.4.3一一对应。

5.5 链接的重定位过程分析

主要通过两个文件之间的不同来分析具体过程发生了什么。

5.5.1函数引入

      dhello.txt中,文件从main开始,没有多余的内容。而hello中,前面有了

大量引入的函数。这说明链接过程已经把外部函数等等内容全部链接成一个

大文件了。

5.5.2实际地址

      dhello中,所有命令的地址,都是根据main的地址往下简单推演的。而

main的地址是0。而在hello中,每个命令都有了自己的虚拟地址,而且main

也被赋予了一个实际执行中地址。

     

5.5.1 main函数的对比

objdump -d -r hello 分析hellohello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

      同时,对全局变量的引用也有了具体的偏移量。

5.5.2全局变量引用对比

      这些变化实际上说明了,链接的过程即实现了符号的解析,把每个符号引

用和符号定义联系了起来,并实现了重定位,将每个符号定义和一个内存位置

关联了起来。

5.6 hello的执行流程

根据前几节的结果比对,得到结果如下:

函数名

地址值(后6位)

_init

401000

.plt

401020

puts@plt

401090

printf@plt

4010a0

getchar@plt

4010b0

atoi@plt

4010c0

exit@plt

4010d0

sleep@plt

4010e0

_start

4010f0

_dl_relocate_static_pie

401120

main

401125

__libc_csu_init

4011c0

__libc_csu_fini

401230

_fini

401238

5.6 hello执行流程表

5.7 Hello的动态链接分析

对于动态共享链接库中的PIC函数,编译器无法预测该函数的运行时地址,需要添加重定位记录并等待动态链接器处理。 为避免在运行时修改调用模块的代码段,链接器使用延迟绑定策略。重定位需要考察函数的plt数组。

got.plt就是这个过程的关键。

我们对00404000这个地址的内容进行考察,我们知道这是 .got.plt的地址 

5.7运行前后的对比

可以观察到运行之后,其内容发生了变化。库函数通过got ,plt来计算正确的地址,并调用链接器来链接。

5.8 本章小结

本章粗略的介绍了链接的过程和内容,通过对反编译文件的比对我们知道了链接过程中究竟发生了什么,重定位又改变了什么,对编译有了更好的了解。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是一个执行中的程序的实例。每个程序都运行在某个进程的上下文中,上下文指的是程序运行需要的各种状态。而在使用中,我们观察到计算机似乎是在一条条的执行我们的指令,每个程序都在独自使用计算机的资源,并完全占用了这些资源,这种假象就是通过进程来实现的;

作用:在现代系统上运行一个程序时,我们会得到一个假象,好像我们的程序是系统中唯一运行的程序一样。我们的程序好像独占处理器和内存。处理器好像无间断地一条接一条执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象是通过进程的概念提供的。进程提供给应用程序的关键抽象:1)一个独立的逻辑控制流,提供一个程序独占处理器的假象;2)一个私有的地址空间,提供一个程序独占地使用内存系统的假象。

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

Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它类似于DOS下的command.com和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。

shell代表用户运行其他程序,其读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

处理流程:

1、从终端读入用户输入的字符串;

2、解析输入的字符串,转化为若干个参数;

3、参数的首位是命令名,判断是否系统内置函数;

4、如果是,则执行内置命令;

5、如果不是,则创建一个子进程,并在子进程中运行程序;

6、判断是否要求在后台执行;

7、若是,则返回循环顶部,等待下一条指令;

8、若不是,则等待该进程结束

6.3 Hello的fork进程创建过程

hello执行的过程中,由于其并不系统内置,因此父进程会通过fork函数来创建一个子进程来运行hello。这个子进程拥有与父进程完全相同的上下文,但二者的执行是相对独立的,可以说,系统创建了一个父进程的副本。并且,二者之间可以并发的运行。二者主要的不同就是PID并不相同。

6.4 Hello的execve过程

execve的作用是将程序加载到当前进程。运行时,其创建一个内存映像,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段。接下来,加载器跳转到程序的入口,_start函数的地址。_start函数调用系统启动函数,_libc_start_main,,初始化环境,调用用户层的main函数,处理main函数返回值,并且在需要的时候返回给内核。

具体的说,其做了以下的工作:

1、删除已存在的用户区域:删除当前进程虚拟地址的用户部分已存在的区域结构

2、映射私有区:为新程序的代码、数据、bss和栈区域创建新的区域结构

3、映射共享区域:如果程序与共享对象链接,那么这些对象都是动态链接

      到这个区域的,然后再映射到用户虚拟地址空间中的。

4、设置程序计数器:设置当前进程上下文中的PC ,使其指向代码区域的入口

6.5 Hello的进程执行

在进行描述之前,我们先介绍一些基本概念:

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

进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片

调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度

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

之后我们结合源程序中的sleep函数来说明,其可以很好的说明执行的流程:

6.5 模式切换的说明

初始时刻,程序的运行处在用户模式,当运行到sleep函数时,转换为内核模式,控制权转移到内核手上,程序暂停,直到收到相关信号,回到用户模式,继续运行。这中间的模式切换就涉及到了上下文的切换,且这个过程中,进程被分为了多个时间片来运行。

6.6 hello的异常与信号处理

hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。

中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。

陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。

故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。

终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。

处理方法如下:

6.6.1中断的处理

6.6.2陷阱的处理

6.6.3故障的处理

6.6.4终止的处理

6.6.5回车&长按回车

回车按完输出会多一行,如果长按可能结束并等待下一次输入

6.6.6 输入Ctrl-C

输入Ctrl-C后,程序停止。这是由于内核向前台进程发送一个SIGINT信号,前台进程终止,内核再向父进程发送一个SIGCHLD信号,通知父进程回收子进程,子进程被终止。

6.6.7 输入CTRL-Z

当输入Ctrl+Z时,内核向前台进程发送一个SIGSTP信号,前台进程被挂起。这时可以输入命令来了解一些信息。

图6.6.8 挂起后输入ps

      输入ps可以查询当前进程的PID,查看状态。

6.6.9 挂起后输入jobs

      输入jobs得到被挂起的进程的jid以及状态。

6.6.10 挂起后输入pstree

输入ptree可以显示当前的进程树,详细显示从开机开始的各个进程父子关系,以一颗树的形式展示。

6.6.11挂起后输入fg

输入fg将被挂起的hello重新调到前台运行,输出内容

运行kill -9 5888 终端被关闭。

6.7本章小结

本章深入了程序的运行过程,讲到用户如何通过shell运行程序,以及程序时如何被加载到进程当中的。我们还讨论了程序运行如何对信号进行处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。其又叫相对地址。一个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。hello程序中给你的地址大都是逻辑地址。

线性地址:是逻辑地址到物理地址变换之间的中间层。代码的运行会产生逻辑地址,逻辑地址再加上一个基地址就可以生成线性地址。

虚拟地址:在了解虚拟内存之前,需要知道的是虚拟内存的概念。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美结合,他为每个程序提供了一个大的、一致的和私有的地址空间。虚拟地址就是在这个背景下产生的,被加载到内存中的地址,就是虚拟地址。

物理地址:计算机系统的主存被组织成一个多个连续字节大小的单元组成的数组,每字节都有一个独立的物理地址。它是物理内存中实际对应的地址

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。我们知道,要从逻辑地址转换为线性地址,需要知道偏移量和段首地址。段标识符是一个16字节的字段,其前13位可以在段描述符表中找到一个具体的端描述符,这个描述符就包括了段首地址。通过TI我们可以知道段描述符存放于GDT中,还是LDT中。找到段首地址,与偏移量相加就得到了线性地址。

                           7.2 逻辑地址的组成

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

虚拟地址到物理地址的转化与翻译是依靠页式管理来实现的。线性地址由虚拟页号VPN和虚拟页偏移VPO组成。页式管理将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。CPU通过VPN得到物理页号,再通过页内偏移,得到物理地址。

7.3页式管理流程

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

VA:虚拟地址 PA:物理地址

每次CPU产生一个虚拟地址,内存管理单元就必须查阅一个页表条目,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据。然而,许多系统都试图消除即使是这样的开销,这就需要使用TLB,翻译后备缓存器。

在这个过程中, VPN被拆分成TLBTTLBI,在TLB表寻找,若TLB命中,则把其与VPO连接作为PA。否则把VPN拆分成K个小VPN,在K页页表里查找,直到找到为止。流程图见图7.3

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

MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则需到低一级Cache(若L3 cache中找不到则到主存)中取出相应的块将其放入当前cache中,重新执行对应指令,访问要找的数据。

这样的Cache读写机制,实现了从CPU寄存器到L1高速缓存,再到L2高速缓存,再到L3高速缓存,再到物理内存的访问,有效的提高了CPU访问物理内存的速度。

7.5 三级cache流程

7.6 hello进程fork时的内存映射

fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve在当前进程下加载新程序,根据第六章的分析,我们知道他做了以下工作:

1、删除已存在的用户区域:删除当前进程虚拟地址的用户部分已存在的区域结构

2、映射私有区:为新程序的代码、数据、bss和栈区域创建新的区域结构

3、映射共享区域:如果程序与共享对象链接,那么这些对象都是动态链接

      到这个区域的,然后再映射到用户虚拟地址空间中的。

4、设置程序计数器:设置当前进程上下文中的PC ,使其指向代码区域的入口

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

DRAM缓存不命中称为缺页。

缺页故障:进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,这就是缺页故障。

缺页中断处理:对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。

其具体的流程如下:
      1、处理器生成一个虚拟地址,并把它传送给内存管理单元

2、内存管理单元生成地址,并从高速缓存/贮存请求得到它

3、高速缓存/主存向内存管理单元返回页表条目。

4、页表条目中的有效位是零,所以内存管理单元触发了-次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。

5、缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。

6、缺页处理程序页面调入新的页面,并更新内存中的条目

7、缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺 页的虚拟地址重新发送给内存管理单元。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存会将所请求字返回给处理器。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对于每个进程,内核负责维护一个变量叫brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块都是一个连续的虚拟内存片有两种状态(已分配或空闲)顾名思义,已分配的块已经被进程所使用,而空闲块可以用来分配,这时他就变成了一个已分配块,而已分配的块只有在被释放才能重新在使用。

一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。Malloc用的是显式分配器。

其在运行时有严格的要求:

1、分配器必须可以处理任意请求序列,对于用户来说执行的mallocfree的数量未必对等,只要释放的块当前是已被分配的有应该响应。

2、为了提高分配的效率,分配器对于申请请求必须迅速响应,不能重新排列或者缓冲请求。

3、分配器必须对齐块,使得他们可以保存任何的数据类型。

4、分配器只能操作修改空闲块对于已分配的块,一旦一个块被分配,不允许改变大小及一些块的固有信息,即使块中有剩余也不能压缩

7.10本章小结

本章我们了解了程序运行时的存储管理,说明了地址的变换和内存如何被访问,也具体了解了程序运行时如何建立内存映射。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

8.2 简述Unix IO接口及其函数

接口就是连接CPU与外设之间的部件,它完成CPU与外界的信息传送。

8.2.1打开文件:int open (char *filename, int flags, mode_t  mode)

       一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

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

8.2.2关闭文件:int close(int fd)

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

      关闭文件是通知内核你要结束访问一个文件。

8.2.3读文件:ssize_t read (int fd, void *buf, size_t n)

      一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k>=m 时执行读操作会触发一个称为EOF 的条件,应用程序能检测到这个条件

read函数从描述符为fd的当前文件复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量

8.2.4 写文件:ssize_t write (int fd, const void *buf, size_t n);

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

8.3 printf的实现分析

先观察函数的内容:

8.3.1printf函数体

可以看到其参数个数不确定。函数中的va_list对象的内容指的就是第一个参数的地址。接着下一句调用了函数vsprintf,其内容如下:

8.3.2vsprintf函数体

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

接下来分析write函数。

write函数把bufi个元素写入中断,然后从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80syscall.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

8.4 getchar函数体

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

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

8.5本章小结

本章主要介绍了IO管理的作用,以及输入输出函数是如何被实现的。

结论

当我们编写hello.c时,我们不曾知道这个简单的程序在实际过程中的运行需要经过那么多的波澜。

它先经了预处理,对我们写下的代码进行简单的优化。之后它经历了编译的过程,使我们书写的高级语言变为汇编语言,为接下来转变为机器可以执行的语言做了准备。然后,它又经过汇编器将hello.s转变为可重定位的二进制目标程序,最后,通过链接器,他才真正成为了机器可以执行的程序。

在这个过程中,机器是如何将这个复杂的过程一步步进行的呢?它又是如何执行我们的命令的呢?这就涉及到了进程在计算机中的运用以及shell在人机交互中发挥的作用,而且,程序的运载也离不开forkexecve的帮助。

但这里的运行也涉及到储存的管理。因为程序本身在计算机中需要被储存,要想弄清它的运行离不开对储存的研究。我们详细的讲述了虚拟地址是如何一步步的被转化为物理地址并与数据建立关联的,也更加详细的解释了forkexecve究竟做了什么。

在程序准备好之后,我们就要实现我们需要的功能了:输入和输出。

我们从命令行中的输入被写入计算机,再经过printf函数调用出来,通过各种硬件、软件的配合,才能使我们想要的输出在屏幕上展现出来。

至此,我们就一步步的看完了hello.c从出生到结束的整个进程,我们终于可以说“我懂hello了”。

附件

文件名

作用

hello.c

源文件代码

hello.i

预处理文件,用于说明

hello.s

编译文件,用于说明

hello.o

汇编文件,用于说明

hello.out

链接后的结果1

hello

链接后的结果2

hello.txt

hello.out的反汇编文件

hellof.txt

helloelf格式文件

dhello.txt

hello.o的反汇编文件

elf.txt

hello.oelf格式文件

参考文献

[1]https://www.cnblogs.com/yyt-hehe-yyt/p/9015219.html

[2] 逆向知识第十二讲,识别全局变量,静态全局变量,局部静态变量,以及变量 https://www.cnblogs.com/iBinary/p/7912427.html.

[3] 关于可重定位目标文件的格式与其符号表的概述

https://blog.csdn.net/weixin_43996099/article/details/101678663

[4]ELF文件头结构https://blog.csdn.net/king_cpp_py/article/details/80334086

[5] 深入理解计算机系统,Computer Systems:A Programmer's Perspective (美)布赖恩特(Bryant,R.E.

[7]  shell 命令行处理流https://blog.csdn.net/weixin_34259559/article/details/93086741

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值