HIT-ICS2022大作业

计算机科学与技术学院

2021年5月

摘  要

通过对hello程序从开始到结束的过程分析,简要回顾本学期所学的知识。将书上的知识与实际操作结合,对程序的每一步动作进行剖析,加深对计算机系统的理解。

关键词:hello的一生;P2P;02O;                           

目  录

第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:一开始hello只是存储在磁盘上的文本文件,在编译器编译时,先由预处理器处理生成hello.i文件,再由编译器将hello.i文件生成hello.s文件,此时还都是文本形式。生成后的hello.s由汇编器生成hello.o,即可重定位文件,再经过链接器的链接后生成可执行文件hello。

       O2O:shell用内置命令调用fork函数为hello创建出进程,并通过execeve在上下文中加载和运行hello,cpu读取指令在流水线中执行,并经过虚拟地址到物理地址的映射访问内存。进程结束后由父进程回收这一进程,由内核清除,hello的一生结束。

1.2 环境与工具

        硬件环境: cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz

                       Ram: 16.0(15.8)Gb

        软件环境: win10 64位; ubuntu 20.04; gcc 11.2.0 ; visual studio2022; gdb、readelf、vim,etc.

1.3 中间结果

hello.i                        预处理得到的文本文件

hello.s                        编译得到的汇编程序

hello.o                       汇编后生成的可重定位目标文件

hello                          链接后生成的可执行目标文件

elf.txt                         hello.o的elf格式文件

elf2                             hello的elf格式文件

helloDump.txt           hello.o反汇编代码文件

helloDump2               hello反汇编代码文件

1.4 本章小结

         这一章简要描述了hello在执行的过程中的流程和实验的软硬件条件以及实验中产生的文件。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程

作用:1.程序的预处理过程就是将预处理指令(可以简单理解为#开头的正确指令)转换为实际代码中的内容(替换)。

2.例如#include<stdio.h>,这里是预处理指令,包含头文件的操作,将所包含头文件的指令替代。

3.如果头文件中包含了其他头文件,也需要将头文件展开包含。

2.2在Ubuntu下预处理的命令

cpp hello.c >hello.i,如图2.2:

 

图2.2预处理命令

2.3 Hello的预处理结果解析

预处理后的文件高达3060行,而我们在hello.c文件中的主函数部分只占最后的14行,并且注释部分已经被去掉,如图2.3.1:

图2.3.1预处理主函数

 而这之前的代码全部为原本以’#’开头的预处理指令的展开,并且还出现了更多的头文件,如图2.3.2:

 图2.3.2预处理头文件(部分)

         这是因为原本#include的三个头文件内,也用#include指令包含了更多的头文件。

2.4 本章小结

            本章对hello程序的预处理部分进行了描述和分析,包括宏替换,头文件展开等

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:把预处理后的.i文件通过编译成为汇编语言,生成.s文件,即把代码从C语言转换成汇编语言,这是GCC编译器完成的工作。

作用:编译器检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,编译器把代码翻译成汇编语言。

3.2 在Ubuntu下编译的命令

gcc –S hello.i –o hello.s,如图3.2:

 图3.2.编译指令

3.3 Hello的编译结果解析

3.3.1 字符串常量

字符串常量,如printf的双引号中的字符串常量,存储在.rodata段,如图3.3.1:

 图3.3.1.字符串常量的存储

3.3.2 数字常量和符号常量

数字常量和符号常量保存在.text段中,如循环比较变量7、比较常量4和函数名main等,如图3.3.2:

 图3.3.2.数字和符号常量存储方式

3.3.3 局部变量

     局部变量存储在栈中或直接存储在寄存器中,本程序中的三个局部变量均保存在栈中:

     ① int i:由于rbq中存储了栈顶rsp的地址,对rbq的操作可以直接作用在rsp上,于是i被保存到了-4(%rbq),也就是rsp-4的位置,可见代码第19、31行,如图3.3.3-1:

 图3.3.3-1局部变量i

     ②argc:代表输入的变量的个数,保存在-20(%rbq),即栈中rsp-20的位置,之后与4比较,如图3.3.3-2:

 图3.3.3-2局部变量argc

     ③argv:是一个保存着输入变量的数组,数组开头在栈中rsp-32的位置,之后在循环体(L4)中进行操作,如图3.3.3-3:

图3.3.3-3 局部变量argv

  3.3.4 算术操作

     此程序直接在寄存器上进行操作,比如每次循环结束后的i++,如图3.3.4:

图3.3.4算术操作i

  3.3.5 关系操作

     直接用栈中保存的局部变量的值与立即数进行比较,再条件跳转到相应的指令:

           ①参数argc:直接与立即数4进行比较,如图3.3.5-1:

图3.3.5-1 条件比较参数argc

          ②循环变量i:赋初值0,每一次循环结束时+1,条件为i≤7,如图3.3.5-2:

 

图3.3.5.-2 条件比较循环变量i

  3.3.6数组/指针/结构操作

     此程序只有一个指针数组argv,其值存储在栈中。argv[1]存储在rsp-24的位置,argv[2]存储在rsp-16的位置,并将*argv[1]赋给rdx,*argv[2]赋给rax,如图3.3.6:

图3.3.6 有关数组argv的操作

    3.3.7 控制转移

      ①if(argc != 4):将存储在rsp-20的argc与立即数4比较,相等则跳转到L2,如图3.3.7-1:

图3.3.7-1 if控制转移

      ②for(i=0;i<8;i++):将存储在rsp-4的局部变量i与7比较,小于等于7则继续循环(循环体L4),否则退出循环,如图3.3.7-2:

图3.3.7-2 for控制转移

      3.3.8 函数调用

           ①main函数:

                  参数:int argc,char *argv[ ],前者最初存储在edi中,然后在栈中,后者最初在rsi中,然后在栈中,如图3.3.8-1:

                  返回值:0,将eax设置为0然后ret,如图3.3.8-2:

图3.3.8-1 参数argc和argv

图3.3.8-2 返回值

           ②printf函数:第一次调用是在argc≠4时,直接输出字符串常量,如图3.3.8-3:

图3.3.8-3 printf第一次调用

                  第二次调用是在for循环体内,用到字符串常量,访问*argv[1]和*argv[2]的值,如图3.3.8-4:

图3.3.8-4printf 第二次调用

           ③atoi函数:参数为*argv[3]的值,返回对应的整型变量,但不一定涉及数据类型转换,如图3.3.8-4:

图3.3.8-4 atoi调用

           ④sleep函数:参数为atoi的返回值,即把存在rax中返回值赋给rdi用作参数,如图3.3.8-5:

图3.3.8-5 sleep调用

           ⑤exit函数:参数在edi里,直接赋值1即可,如图3.3.8-6:

 图3.3.8-6 exit调用

             ⑥getchar函数:直接调用,如图3.3.8-7:

图3.3.8-7 getchar调用

3.4 本章小结

以分析汇编代码的形式描述了整个程序中数据的存储方式以及各种操作的实现方法

(第32分)

第4章 汇编

4.1 汇编的概念与作用

       概念:汇编器将hello.s文件转换为机器语言指令,将结果保存在可重定位目标文件hello.o中

       作用:将汇编代码转换为机器可以理解的二进制机器码形式

4.2 在Ubuntu下汇编的命令

       gcc -c hello.s -o hello.o,如图4.2:

图4.2 汇编命令

4.3 可重定位目标elf格式

       4.3.1 指令 先用readelf –a hello.o > ./elf.txt指令导出elf文件:如图4.3.1

图4.3.1 生成可重定位目标文件elf格式

   

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

图4.3.2 elf头

       4.3.3 节头表 描述了.o文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个固定大小的条目。具体内容如下图4.3.3所示:

图4.3.3节头表

        4.3.4重定位节:重定位节中包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对这些变量符号进行修改。链接的时候链接器会根据重定位节的信息对外部变量符号决定选择何种方法计算正确的地址,通过偏移量等信息计算出正确的地址。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,atoi,sleep,getchar这些符号同样需要与相应的地址进行重定位。具体重定位节的信息如下图4.3.4所示:

图4.3.4 重定位节信息

        4.3.5 符号表:.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。例如本程序中的getchar、printf、exit等函数名都需要在这一部分体现,具体信息如下图4.3.5所示:

图4.3.5符号表信息

4.4 Hello.o的结果解析

反汇编代码如下:

hello.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:

   0:  f3 0f 1e fa                endbr64

   4:  55                          push   %rbp

   5:  48 89 e5               mov    %rsp,%rbp

   8:  48 83 ec 20              sub    $0x20,%rsp

   c:  89 7d ec                mov    %edi,-0x14(%rbp)

   f:   48 89 75 e0             mov    %rsi,-0x20(%rbp)

  13:  83 7d ec 04              cmpl   $0x4,-0x14(%rbp)

  17:  74 16                je     2f <main+0x2f>

  19:  48 8d 3d 00 00 00 00       lea    0x0(%rip),%rdi        # 20 <main+0x20>

                  1c: R_X86_64_PC32 .rodata-0x4

  20:  e8 00 00 00 00           callq  25 <main+0x25>

                  21: R_X86_64_PLT32      puts-0x4

  25:  bf 01 00 00 00            mov    $0x1,%edi

  2a:  e8 00 00 00 00           callq  2f <main+0x2f>

                  2b: R_X86_64_PLT32      exit-0x4

  2f:   c7 45 fc 00 00 00 00 movl   $0x0,-0x4(%rbp)

  36:  eb 48                 jmp    80 <main+0x80>

  38:  48 8b 45 e0             mov    -0x20(%rbp),%rax

  3c:  48 83 c0 10             add    $0x10,%rax

  40:  48 8b 10               mov    (%rax),%rdx

  43:  48 8b 45 e0             mov    -0x20(%rbp),%rax

  47:  48 83 c0 08             add    $0x8,%rax

  4b:  48 8b 00               mov    (%rax),%rax

  4e:  48 89 c6               mov    %rax,%rsi

  51:  48 8d 3d 00 00 00 00       lea    0x0(%rip),%rdi        # 58 <main+0x58>

                  54: R_X86_64_PC32 .rodata+0x22

  58:  b8 00 00 00 00           mov    $0x0,%eax

  5d:  e8 00 00 00 00           callq  62 <main+0x62>

                  5e: R_X86_64_PLT32      printf-0x4

  62:  48 8b 45 e0             mov    -0x20(%rbp),%rax

  66:  48 83 c0 18             add    $0x18,%rax

  6a:  48 8b 00               mov    (%rax),%rax

  6d:  48 89 c7               mov    %rax,%rdi

  70:  e8 00 00 00 00           callq  75 <main+0x75>

                  71: R_X86_64_PLT32      atoi-0x4

  75:  89 c7                mov    %eax,%edi

  77:  e8 00 00 00 00           callq  7c <main+0x7c>

                  78: R_X86_64_PLT32      sleep-0x4

  7c:  83 45 fc 01              addl   $0x1,-0x4(%rbp)

  80:  83 7d fc 07              cmpl   $0x7,-0x4(%rbp)

  84:  7e b2                jle    38 <main+0x38>

  86:  e8 00 00 00 00           callq  8b <main+0x8b>

                  87: R_X86_64_PLT32      getchar-0x4

  8b:  b8 00 00 00 00           mov    $0x0,%eax

  90:  c9                   leaveq

  91:  c3                   retq  

与hello.s对比,有以下不同:

①数字的进制不同,hello.s中数字以十进制表示,反汇编代码中以十六进制表示;

②控制转移的表示不同,hello.s中跳转目标用段名表示,反汇编代码中以具体地址表示;

③函数调用方式不同,hello.s中调用的函数直接用名字表示,反汇编代码中用main函数+偏移量表示,并且由于还未链接,调用函数的指令中,操作数都为0.

4.5 本章小结

描述了从hello.s生成的可重定位目标文件hello.o的内容,通过与hello.s文件的对比,深入理解指令为了链接做出的变化。

(第41分)

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-1 ld链接

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

命令:readelf -a hello > elf2,如图5.3-1,

图5.3-1 readelf命令

5.3.1 ELF头:包含内容与汇编中4.3.2节展示的类似,详细内容如图5.3.1,

图5.3.1 ELF头

5.3.2 节头表:section Headers描述了各节的信息,其中Name对应各节的名字,Type对应记录了每节的类型 ,address对应了这一节的信息在虚拟内存中的存储位置。Offset对应了每一节相对于0x400000的偏移地址。Size记录了每一节的大小。Align记录了对齐位数,Info记录这一节的信息,后两位对应类型,前六位对应在symtab节中的ndx值,如图5.3.2,

图5.3.2 部分节头表

 5.3.3程序头表:program Headers可以看到可执行文件共分为8个节,每个节对应的偏移地址,虚拟地址位置,物理地址位置,文件大小,存储大小,标志和对齐方式都存储在该节中。Program header告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。

图5.3.3 程序头表

5.4 hello的虚拟地址空间

       使用edb打开hello可执行文件,可以在edb的Data Dump窗口看到hello的虚拟地址空间分配的情况,如图5.4,

图5.4 edb加载hello

      

       与图5.3.2和图5.3.3对比,可以发现,data dump中的0x401000在program headers里对应init.,为程序开始处;data dump在0x402000处停止,对应section headers里的.rodata部分……PHDR存储的是程序头表,INTERP存储的是程序执行前需要调用的解释器,LOAD存储的是程序目标代码和常量信息,DYNAMIC存储的是动态链接器所使用的信息,NOTE存储的是辅助信息,GNU_EH_FRAME存储的是保存异常信息,GNU_STACK存储的是使用系统栈所需要的权限信息。GNU_RELRO存储的是保存在重定位之后的只读信息的位置。

5.5 链接的重定位过程分析

命令:objdump -d -r hello > helloDump2

图5.5 objdump命令

与hello.o的反汇编代码比较,主要有以下不同:

①程序添加了许多动态链接库中的函数,程序原先调用的库函数都被复制到了程序的代码中来,如图5.5-1,

图5.5-1 部分hello反汇编代码

    ②重定位:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。比如从图5.5-1还可以看出,原本以0代替的跳转地址已经变成了具体的虚拟地址,如puts段的机器指令中,目的地址已经变成了0x2f7d。

    ③hello中增加了.init和.plt节,和一些节中定义的函数,如图5.5-2,

图5.5-2 .init和.plt节

链接的过程:符号解析和重定位。

1、符号解析:目标文件定义和引用符号,符号解析将每个符号引用和一个符号定义关联起来。

2、重定位:编译器和汇编器生成从0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

5.6 hello的执行流程

根据反汇编代码可以看出执行函数及虚拟内存地址如下:

401000 <_init>

401020 <.plt>

401030 puts@plt

401040 printf@plt

401050 getchar@plt

401060 atoi@plt

401070 exit@plt

401080 sleep@plt

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>

5.7 Hello的动态链接分析

首先观察elf中.got.plt节的内容,如图5.7-1,

图5.7-1 elf中.got.plt节

1、执行dl_init之前的.plt,如图5.7-1,

图5.7-1 执行dl_init之前.plt的内容

2、执行dl_init之后的.plt,如图5.7-2,

图5.7-2 执行dl_init之后的.plt内容

5.8 本章小结

本章介绍了链接的概念与作用、hello的ELF格式,hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。通过实例分析了重定位过程、加载以及运行时函数调用顺序以及动态链接。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

概念:①狭义定义:进程就是一段程序的执行过程。②广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

作用:显示当前内存中运行的程序,服务等。通过进程可以判断某个程序占用多少CPU和内存使用量,还可以通结束进程来结束无法关闭的程序。还可以判断病毒木马等。

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

作用:读取用户输入的字符串命令, 解释并且执行命令. 提供运行其他程序的的接口.它可以是交互式的, 即读取用户输入的字符串;也可以是非交互式的, 即读取脚本文件并解释执行, 直至文件结束.

处理流程:

1. 将命令行分成由 元字符(meta character) 分隔的 记号(token):

元字符包括 SPACE, TAB, NEWLINE, ; , (, ), <, >, |, &

记号 的类型包括 单词,关键字,I/O重定向符和分号。

2. 检测每个命令的第一个记号,看是否为不带引号或反斜线的关键字。如果是一个 开放的关键字,如if和其他控制结构起始字符串,function,{或(,则命令实际上为一复合命令。shell在内部对复合命令进行处理,读取下一个命 令,并重复这一过程。如果关键字不是复合命令起始字符串,而是如then等一个控制结构中间出现的关键字,则给出语法错误信号。

3. 依据别名列表检查每个命令的第一个关键字。如果找到相应匹配,则替换其别名定义,并退回第一步;否则进入第4步。

4. 执行大括号扩展,例如a{b,c}变成ab ac

5. 如果~位于单词开头,用$HOME替换~。使用usr的主目录替换~user。

6. 对任何以符号$开头的表达式执行参数(变量)替换

7. 对形如$(string)或者`string` 的表达式进行命令替换

这里是嵌套的命令行处理。

8. 计算形式为$((string))的算术表达式

9. 把行的参数替换,命令替换和算术替换 的结果部分再次分成单词,这次它使用$IFS中的字符做分割符而不是步骤1的元字符集。

10. 对出现*, ?, [ ]对执行路径名扩展,也称为通配符扩展

11.  按命令优先级表(跳过别名),进行命令查寻。

   先作为一个特殊的内建命令,接着是作为函数,然后作为一般的内建命令,最后作为查找$PATH找到的第一个文件。

12. 设置完I/O重定向和其他操作后执行该命令。

下面为图示,如图6.2:

图6.2

6.3 Hello的fork进程创建过程

       父进程通过调用fork函数创建一个新的、处于运行状态的子进程。子进程返回0,父进程返回子进程的PID,新创建的子进程几乎但不完全与父进程相同:①子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本;②子进程获得与父进程任何打开文件描述符相同的副本;③子进程有不同于父进程的PID

fork函数被调用一次,返回两次。

6.4 Hello的execve过程

       execve函数在当前进程的上下文中加载并运行一个新程序。

execve函数加载并运行可执行目标文件hello。且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve函数才会返回调用程序。

在execve加载了hello之后,它调用启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下形式的原型 int main(int argc , char **argv , char **envp)。

在main开始执行时,用户栈的组织结构如图6.4所示:

图6.4

6.5 Hello的进程执行

    1、逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。

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

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

3、上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

4、简单看hello sleep进程调度的过程:当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行1)保存以前进程的上下文2)恢复新恢复进程被保存的上下文,3)将控制传递给这个新恢复的进程 ,来完成上下文切换。

如图6.5所示,hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时(2.5secs)发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

图6.5 hello的上下文切换

6.6 hello的异常与信号处理

6.6.1 异常的类型和处理方法,如图6.6.1:

图6.6.1异常类型和处理方法

① 中断的处理:中断处理程序返回到下一条指令处,如图6.6.1-1,

图6.6.1-1中断处理

② 陷阱的处理:将控制返回到下一条指令,如图6.6.1-2,

图6.6.1-2陷阱处理

③ 故障的处理:要么重新执行引起故障的指令(已修复),要么终止,如图6.6.1-3,

图6.6.1-3故障处理

④终止的处理:中止当前程序。如图6.6.1-4,

图6.6.1-4终止处理

6.6.2 hello执行过程中的异常与信号

  ① 不停乱按,产生中断异常,被输入的乱码先进入缓冲区,程序继续正常运行,结束后乱码被当作命令处理,但程序无法识别,如图6.6.2-1,

图6.6.2.-1不停乱按

      ② 按下ctrl-Z,发生中断异常,产生SIGTSTP信号,hello进程收到信号后停止并挂起,此时另一个进程处于用户模式下,等待下一次信号输入,如图6.6.2-2,

图6.6.2-2 ctrl-Z命令

      ③ 此时输入ps命令,可以发现程序处于挂起状态,如图6.6.2-3,

图6.6.2-3 ps命令

      ④ 输入jobs命令,可以看到之前的作业处于前台且停止状态,如图6.6.2-4,

图6.6.2-4 jobs命令

      ⑤ 输入pstree命令,得到信息(图为一部分),如图6.6.2-5,

图6.6.2-5 pstree命令

      ⑥ 输入fg命令,产生中断异常。由于为前台作业,输入的命令为fg  1,进程收到SIGCONT信号继续运行,输出剩余的六个信息,输出结束后手动getchar结束程序,如图6.6.2-6,

图6.6.2-6 fg命令

      ⑦ 输入kill命令,ctrl-Z后查看进程pid,再输入kill -9 5107杀死hello进程,之后通过jobs和ps指令可以发现进程已经被杀死,如图6.6.2-7,

图6.6.2-7 kill命令

      ⑧ 输入ctrl-C命令,产生终止异常,发出SIGINT信号,hello进程在收到信号后直接停止,如图6.6.2-8,

 

图6.6.2-8 ctrl-C命令

6.7本章小结

       本章主要介绍了hello可执行文件的执行过程,包括进程创建、加载和终止,以及通过键盘输入等过程。Hello完成了P2P的过程,这一整个过程中需要各种各样的异常和中断等信息。程序的高效运行离不开异常、信号、进程等概念,正是这些机制支持hello能够顺利地在计算机上运行。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

        1、逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。从hello的反汇编代码中看到的地址,它们需要通过计算,通过加上对应段的基地址才能得到真正的地址,这些便是hello中的逻辑地址。
    2、线性地址:是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。
    3、虚拟地址:有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
    4、物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。在hello的运行中,在访问内存时需要通过CPU产生虚拟地址,然后通过地址翻译得到一个物理地址,并通过物理地址访问内存中的位置。

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

逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中,如图7.2,

图7.2 intel段式管理

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

页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址,如图7.3,

图7.3 页式管理

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

         7.4.1 TLB:简单来说就是页表的硬件缓存,是CPU的硬件部件。一个core一套(一般分指令TLB和数据TLB)。没有TLB的时候,CPU的每一次虚实转换都需要访问存放在内存里的页表。和CPU的L1、L2、L3的缓存思想一致,既然进行地址转换需要的内存IO次数多,且耗时。于是在CPU里把页表尽可能地存到cache,所以就有了TLB(Translation Lookaside Buffer),专门用于改进虚拟地址到物理地址转换速度的缓存。其访问速度非常快,和寄存器相当,比L1访问还快。其处理过程大致如下图7.4.1-1和7.4.1-2,

图7.4.1-1虚拟地址中以访问TLB的组成部分

图7.4.1-2 TLB工作机制

7.4.2 四级页表下的VA到PA变换:当TLB与四级页表相结合其地址翻译过程如下:先将这个虚拟地址的VPN分为TLB标记部分和TLB索引部分检查是否再TLB命中如果命中直接取出物理地址,否则的化虚拟地址被划分为4个VPN和一个VPO每个VPN(i)对应了第i级页表的表头索引,通过这个索引最后对应了一个固定的PPN将这个PPN与VPO结合得到新的物理地址,并把这个物理地址的信息存入TLB缓存,如图7.4.2,

图7.4.2 四级页表下的VA到PA

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

       PA被分为CT,Cl,CO三部分,CT对应标志位,Cl为组索引,CO为组内偏移位,按照Cl找到相应的组。然后将CT和相应的组中所有行的标记位进行比较,当PA的标记位和高速缓存行的标记位匹配,块中有对应数据且高速缓存行的有效位是1,则L1命中。否则L1向下一级高速缓存L2取数据,L2未命中则下到L3,直到命中为止,如图7.5,

图7.5 三级cache的物理内存访问

7.6 hello进程fork时的内存映射

①为新进程创建虚拟内存

②创建当前进程的mm_struct、vm_area_struct和页表的原样副本。

③两个进程中的每个页面都标记为只读

④两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)

⑤在新进程中返回时,新进程拥有与调用fork的父进程相同的虚拟内存

⑥随后的写操作通过写时复制机制创建新页面

如图7.6,

图7.6 fork时的内存映射

7.7 hello进程execve时的内存映射

       ①删除已存在的用户区域,创建新的区域结构

②代码和初始化的数据映射到.text和.data区(目标文件提供)

③.bss和栈映射到匿名文件

④设置PC,指向代码区域的入口点

⑤Linux根据需要换入代码和数据页面

如图7.7,

图7.7 execve时的内存映射

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

       7.8.1 缺页故障:访问的虚拟地址不在虚拟地址空间内,或者对一个只读页面进行了写操作,此时发生缺页故障,如图7.8-1所示的段错误和保护异常。进程调用异常处理程序,具体处理操作见图6.6.1-3。

    7.8.2缺页中断:在虚拟地址判断合法后,若访问的虚拟地址有效位为0,发生缺页中断,如图7.8-1所示的正常缺页。内核会选择一个牺牲页,若该页面被修改过,则将 它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新 启动引起缺页的指令,这条指令再次发送VA 到MMU,过程如图7.8.2

图7.8-1 缺页故障与缺页中断处理

图7.8-2 缺页中断处理

7.9动态存储分配管理

7.9.1定义:一种内存管理方法。对内存空间的分配、回收等操作在进程执行过程中进行,以便更好地适应系统的动态需求,提高内存利用率。

分配器的类型:

①显式分配器: 要求应用显式地释放任何已分配的块.。例如,C语言中的malloc 和 free

②隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块。比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage collection)

7.9.2 分配器内存管理方式:

     1、隐式空闲链表:通过头部中的长度字段—隐含地链接所有块,标准块如图7.9.2-1,链表连接方式如图7.9.2-1,

图7.9.2-1标准块

图7.9.2-2 隐式空闲链表

带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的,如图7.9.2-2。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个符合大小的空闲块来放置这个请求块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。在释放一个已分配块的时候需要考虑是否能与前后空闲块合并,减少系统中碎片的出现。

图7.9.2-3带边界标记的隐式空闲链表块

2、显式空闲链表:在空闲块中使用指针连接空闲块,如图7.9.2-4,链表在物理层面的顺序是任意的。显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。放置策略与上述放置策略一致。如图7.9.2-5,

图7.9.2-4 显示空闲链表的块

图7.9.2-5显式空闲链表块

7.10本章小结

本章讨论了四种地址,接着分析了从逻辑地址到线性地址再到物理地址的转换过程。接着描述了从VA到PA的变换、物理内存访问、内存映射、缺页故障和缺页处理、动态存储分配管理。较为完整地阐明了hello的存储管理内容。由于动态存储分配管理并未学习,本章此处只进行简单介绍。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件。文件是用来代表物理设备的。多数物理设备是用来进行输出或输入的,所以必须由某种机制使得内核中的设备驱动从进程中得到输出送给设备。这可以通过打开输出设备文件并且写入做到,就象写入一个普通文件。

在Linux系统下,设备文件是种特殊的文件类型,其存在的主要意义是沟通用户空间程序和内核空间驱动程序。换句话说,用户空间的应用程序要想使用驱动程序提供的服务,需要经过设备文件来达成。Linux系统所有的设备文件都位于**/dev**目录下。

设备管理:unix io接口。将设备映射为文件的方式,允许Unix内核引出一个简单、低级的应用接口。

 Linux/unix IO的系统调用函数很简单,它只有5个函数:open(打开)、close(关闭)、read(读)、write(写)、lseek(定位)。但是系统IO调用开销比较大,一般不会直接调用,而是通过调用Rio包进行健壮地读和写,或者调用C语言的标准I/O进行读写。

8.2 简述Unix IO接口及其函数

1、打开文件:返回一个小的非负整数,即描述符。用描述符来标识文件。每个进程都有三个打开的文件:标准输入(0)、标准输出(1)、标准错误(2)

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

//返回:若成功则为新文件描述符,若出错为-1

2、改变当前文件位置  从文件开头起始的字节偏移量。系统内核保持一个文件位置k,对于每个打开的文件,起始值为0。应用程序执行seek,设置当前位置k,通过调用lseek函数,显示地修改当前文件位置。

    函数:int lseek(int fd, off_t offset, int whence);

           //成功则返回新的文件的偏移量;失败则返回-1.

           //参数offset的解释与whence相关。

若whence为SEEK_SET,则将该文件的偏移量设置为距离当前文件开始处offset字节。

若whence为SEEK_CUR,则将该文件的偏移量设置为距离当前偏移量加offset个字节,此时offset可正可负。

若whence为SEEK_END,则将该文件的偏移量设置为当前文件长度加offser个字节,此时offset可正可负。

3、读文件:从文件拷贝n个字节到存储器,从当前文件位置k开始,将k增加到k+n,对于一个大小为m字节的文件,当k>=m时,读操作触发一个EOF的条件。

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

           //若读取成功,读到文件末尾返回0,未读到文件末尾返回当前读的字节数。若读取失败,返回-1。

           // fd为要读取文件的文件描述符。buf为读取文件数据缓冲区,nbytes为期待读取的字节数,通常为sizeof(buf)。

4、写文件:从存储器拷贝n个字节到文件,k更新为k+n

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

           //若写入成功则返回写入的字节数;失败返回-1.

           // buf为写入内容的缓冲区,ntyes为期待写入的字节数,通常为sizeof(buf)。一般情况下返回值与ntypes相等,否则写入失败。

5、关闭文件:内核释放文件打开时创建的数据结构,并恢复描述符到描述符池中,进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。

    函数:int close(int fd);

           //文件关闭成功返回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接受了一个fmt的格式,然后将匹配到的参数按照fmt格式输出。我们看到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':

            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);

}

可以看到这个函数的作用是将所有参数内容格式化后存入buf,然后返回格式化数组的长度。而另一个函数write是一个输出到终端的系统调用在此不做赘述。

总的来说printf函数的显示过程是:从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是读入函数的一种。它从标准输入里读取下一个字符,相当于getc(stdin)。返回类型为int型,为用户输入的ASCII码或EOF。getchar可用宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。

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

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

8.5本章小结

本章简单讲述了Linux的IO设备管理方式讲述了文件和和unix io的接口介绍了几个unix io接口的函数并结合hello分析了printf和getchar函数,加深对Unix I/O以及异常中断等的了解。

(第81分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

1、预处理,hello.c文件通过预处理器(cpp)的处理,得到了扩展后的源程序文件hello.i

2、编译,hello.i经过编译器(gcc)处理,生成了汇编程序hello.s

3、汇编,hello.s在汇编器as的处理下,生成了可重定位目标文件hello.o

4、链接,链接器将重定位目标文件链接为可执行目标文件hello

5、创建进程,shell接收相应命令,调用fork函数为hello创建进程。

6、加载并运行hello程序,通过execve将它映射到对应虚拟内存区域,并依需求载入物理内存。

7、Hello将在cpu流水线中执行每一条指令,由于hello程序中存在输入与输出,这些部分与printf,getchar函数有关,这些函数与linux系统的I/O设备密切相关,所以会涉及IO管理。

8、程序运行结束后,父进程收到SIGCHLD信号,对hello进行回收,内核把它从系统中清除。

hello结束了它的一生。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

      计算机系统的设计与实现对操作者对相关知识的熟悉程度有相当高的要求,而熟练运用计算机系统的知识可以对程序的行为进行优化

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

附件

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

hello.i                        预处理得到的文本文件

hello.s                        编译得到的汇编程序

hello.o                       汇编后生成的可重定位目标文件

hello                          链接后生成的可执行目标文件

elf.txt                         hello.o的elf格式文件

elf2                             hello的elf格式文件

helloDump.txt           hello.o反汇编代码文件

helloDump2               hello反汇编代码文件

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

参考文献

[1]  《深入理解计算机系统》Randal E.Bryant David R.O’Hallaron 机械工业出版社

[2]  https://blog.csdn.net/u014630623/article/details/89046099

[3]  https://www.cnblogs.com/ztguang/p/12647664.html

[4]  https://zhuanlan.zhihu.com/p/358951580

[5]  https://www.cnblogs.com/AndroidBinary/p/15366466.html

[6]  缺页中断和缺页异常 - 张铁子 - 博客园

[7]  https://blog.csdn.net/z1162565234/article/details/80466842

(参考文献0分,缺失 -1分)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值