关注

哈工大计算机系统2024大作业——Hello的程序人生

大作业

题     目  程序人生-Hello’s P2P 

专       业数学与应用数学+计算机科学与技术双学位

学     号        2023110264         

班     级          23SXS11        

学       生         谢辰阳        

指 导 教 师          史先俊             

计算机科学与技术学院

2024年5月

摘  要

本论文主要是解释一个具体的c语言程序(hello.c)是如何转化为可执行文件以及是如何被终端程序执行的。这个步骤大致分为预处理、编译、汇编、链接、进程管理等。与此同时本文还结合Linux系统(以ubuntu20.04演示)的一些基础知识如虚拟内存,地址空间,时间片,逻辑控制流和UNIX IO等,具体描述了这个过程,体现了hello程序短暂而又精彩的一生。

关键词:计算机系统;汇编语言;链接;Linux;c语言                             

目  录

第1章 概述................................................................................................................ - 4 -

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

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

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

1.4 本章小结............................................................................................................ - 5 -

第2章 预处理............................................................................................................ - 6 -

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

2.2在Ubuntu下预处理的命令............................................................................. - 6 -

2.3 Hello的预处理结果解析................................................................................. - 6 -

2.4 本章小结............................................................................................................ - 7 -

第3章 编译................................................................................................................ - 8 -

3.1 编译的概念与作用............................................................................................ - 8 -

3.2 在Ubuntu下编译的命令................................................................................ - 8 -

3.3 Hello的编译结果解析..................................................................................... - 8 -

3.4 本章小结.......................................................................................................... - 10 -

第4章 汇编.............................................................................................................. - 11 -

4.1 汇编的概念与作用.......................................................................................... - 11 -

4.2 在Ubuntu下汇编的命令.............................................................................. - 11 -

4.3 可重定位目标elf格式.................................................................................. - 11 -

4.4 Hello.o的结果解析....................................................................................... - 13 -

4.5 本章小结.......................................................................................................... - 14 -

第5章 链接.............................................................................................................. - 15 -

5.1 链接的概念与作用.......................................................................................... - 15 -

5.2 在Ubuntu下链接的命令.............................................................................. - 15 -

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

5.4 hello的虚拟地址空间................................................................................... - 17 -

5.5 链接的重定位过程分析.................................................................................. - 18 -

5.6 hello的执行流程........................................................................................... - 20 -

5.7 Hello的动态链接分析................................................................................... - 22 -

5.8 本章小结.......................................................................................................... - 23 -

第6章 hello进程管理...................................................................................... - 24 -

6.1 进程的概念与作用.......................................................................................... - 24 -

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

6.3 Hello的fork进程创建过程........................................................................ - 24 -

6.4 Hello的execve过程.................................................................................... - 25 -

6.5 Hello的进程执行........................................................................................... - 25 -

6.6 hello的异常与信号处理............................................................................... - 26 -

6.7本章小结.......................................................................................................... - 28 -

第7章 hello的存储管理.................................................................................. - 29 -

7.1 hello的存储器地址空间............................................................................... - 29 -

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

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

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

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

7.6 hello进程fork时的内存映射..................................................................... - 32 -

7.7 hello进程execve时的内存映射................................................................. - 32 -

7.8 缺页故障与缺页中断处理.............................................................................. - 32 -

7.9动态存储分配管理.......................................................................................... - 33 -

7.10本章小结........................................................................................................ - 34 -

第8章 hello的IO管理.................................................................................... - 35 -

8.1 Linux的IO设备管理方法............................................................................. - 35 -

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

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

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

8.5本章小结.......................................................................................................... - 38 -

结论............................................................................................................................ - 38 -

附件............................................................................................................................ - 39 -

参考文献.................................................................................................................... - 40 -

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

1.1.1:P2P的过程

       P2P指的是From Program to Process,指的是hell如何从c语言源文件经过一系列处理转化为可执行文件,这一过程主要可以分为四个阶段

  1. 预处理:复制include的内容,展开宏变量,转为hello.i
  2. 编译:翻译成汇编语言,转为hello.s
  3. 汇编:将汇编语言翻译成机器语言,生成可重定位目标程序hello.o
  4. 链接:由链接器ld来把c标准库里面的函数合并到我们的文件中,生成可执行文件hello

1.2 环境与工具

1.2.1硬件环境:X64 CPU;2.20GHz;16G RAM;1024GHD Disk;

1.2.2软件环境:Windows11 64位,VMware Workstation Pro15.5.1,Ubuntu 20.04.4

1.2.3开发及调试工具:gdb;edb;objdump;readelf;Code::Blocks20.03;vscode

1.3 中间结果

  1. hello.i: hello.c预处理后的文件。 用于处理#对应的内容

2)hello.s: hello.i编译后的文件。由编译器生成的汇编代码

3)hello.o: hello.s汇编后的文件

4)helloelf.txt:readelf产生的hello.o的elf分析文件

5) hello_asm1.s:使用objdump生成的hello.o的反汇编文件

6)helloelf2.txt:hello的elf文件

7)hello_asm2.s:使用objdump生成的hello的反汇编文件

8)hello:最终编译生成的可执行文件

9)hello.c:程序源文件

1.4 本章小结

  本章主要是讲述了一个背景,介绍本作业的目的和总结hello的一生。同时列出来实验的环境,开发工具,和中间内容说明以便后续阅读。

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念:预处理是指预处理器在源代码编译前所进行的一系列文本操作,其主要包括删除注释、处理#开头的预处理指令(如复制头文件,展开宏)等

2.1.2预处理的作用1)去除注释:方便后续汇编器生成汇编代码2)进行宏替换:把宏定义的变量直接换成常值。3)展开头文件:将#include所指定的头文件写入并展开。4) 做条件编译:主要与#if系列语句有关,可以在不同条件下执行不同的程序,便于程序跨平台

2.2在Ubuntu下预处理的命令

cpp hello,c > hello.i 或者 gcc hello.c -E -o hello.i

2.3 Hello的预处理结果解析

查看hello.i发现前面是三个引用的头文件的信息,3480行以后是和原文件一样的c语言源码。

2.4 本章小结

本章是hello“程序生”的起点,从最起始的预处理开始,讲解了预处理的概念和作用,展示了ubuntu下预处理操作及操作结果,并对预处理文件hello.i进行了解析,直观反应预处理的作用。

第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念:利用编译软件,从c语言源码生成汇编代码的过程。

3.1.2编译的作用:1)产生汇编代码2)优化源代码的结构

3.2 在Ubuntu下编译的命令

/usr/lib/gcc/x86_64-linux-gnu/9/cc1 hello.i > hello.s或者gcc hello.i -S -o hello.s

3.3 Hello的编译结果解析

3.3.1分析各个段的意义:1).file:声明这个文件的名字是hello.i

2).rodata:只读数据 LC0是那个输入时候参数数量不对的提示词(用法: Hello 学号 姓名 手机号 秒数!) .LC1常量是一个格式串,是printf要使用的

3).globl 表示声明的符号是全局的

4).text表示代码段

5).type 表示这个符号的类型,后面的@function表示他是一个函数

6).LFB6等是函数的标签 .cfi_*是栈的操作可能是调试用的

3.3.2 留局部变量:1)在21行把%edi(argc)放到%rbp-20的位置留出了20Byte 其实argc是int只有4Byte   

2)在22行把%rsi(argv) 放到了%rbp-32的位置。注意这里其实只是放了数组的首地址,即只使用了8Byte

3.3.3跳转:通过比较argc与5的大小,相同的话执行.L2函数

3.3.4循环:1)声明%rbp-4是存一个局部变量,在源码中他是i。

2)这里.L3明显是一个for循环的实现,如果i小于等于8继续执行.L4即循环体。i大于8则调用getchar再结束函数

3)当然这里的je就是一个关系操作

3.3.5对于.L4的实现主要分1)使用栈传递参数如使用-32(%rbp)  

2)立即数算数操作:add $24, %rax把24这个应该是10进制加到rax中

3)数组操作:上面有说到%rbp-32存的是一个字符串数组的首地址故对这个地址加8的n倍即是取数组中下标为n的数

4)寄存器传递参数:在调用printf和sleep函数的时候,汇编代码使用了寄存器传递参数,具体寄存器的值我使用#给出了

5)赋值:movq   -32(%rbp), %rax就是把数组首地址赋值给了rax

6)指针操作: addq     $16, %rax就是一个对rax保存的指针进行操作

      movq  (%rax), %rax则是取出地址对应的值

7)函数调用:call     sleep@PLT  #调用sleep就是调用sleep

3.4 本章小结

本章首先阐明了编译的概念和作用,再在ubuntu下实际进行了编译操作并展示结果,通过按类型、操作分析编译结果文件,生动体现了编译的操作方法和重要作用。

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念:汇编是指把汇编语言编写的程序转换为相匹配的机器语言程序的过程。汇编程序所输入的是用汇编语言编写的源程序,而输出的是用机器语言编写的目标程序。

4.1.2汇编的作用 1)将程序转写为机器语言

2)汇编过程中可从汇编程序得到一个可重定位目标文件,便于后续的链接操作

4.2 在Ubuntu下汇编的命令

As hello.s -o hello.o 或者gcc –c hello.s –o hello.o

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

4.3.1使用命令readelf -a hello.o  > helloelf.txt把hello.o的ELF信息重定向到一个txt文件中下面是结果,及各段的解析

可重定位目标文件的ELF主要包括三个部分:ELF头,节,和节头部表

  1. ELF头:包含页面大小,虚拟地址内存段(节),段大小
  2. .text:机器代码部分
  3. .rela.text:代码的重定位部分
  4. .data:已经初始化的全局变量和静态c变量,
  5. .bss:未初始化的全局变量和静态c变量以及所有被初始化为0的全局变量和静态c变量
  6. .rodata:只读数据,比如上面提到的,提示语句和printf的格式串
  7. .symtab:符号表:内含函数和全局/静态变量名,节名称和位置

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.4.1:使用objdump生成hello_asm1.s

4.4.2对比         从hell.o反汇编来的                     之前生成的

  1. 发现多了机器码,就是多了在汇编代码前面以16进制表示的那些代码

   同时栈分配的空间也有统一使用16进制表示

  1. 函数调用方式不同:汇编代码中,函数的调用直接通过函数名进行;而在机器代码中,call的目标地址是当前指令的下一条地址。机器代码中,对于这种不确定地址的调用,需要在链接时才能确定其地址
  2. 分支跳转方式不同。汇编代码中使用标识符来确定跳转目标;机器代码中经过转写直接使用地址实现跳转。

4.5 本章小结

这一章首先简要说明了汇编在“程序生”中的概念与作用,随后在ubuntu中实际操作了汇编并生成重定向文件(hello.o),对elf文件的内容进行了详细解析。接着还进行了反汇编并对反汇编结果进行阐释,由此衍生,解释了机器代码与汇编代码的关系与不同。

5章 链接

5.1 链接的概念与作用

5.1.1链接的概念:链接是指将各种代码和数据片段(好多的.o可重定位目标文件)整合为一个单一文件的过程,这一文件可以被加载到内存执行,由此使得程序可被执行(生成可执行目标文件)。

5.1.2链接的作用:1)分离编译:使得不同的c语言代码可以先生成自己的.o文件再在这一步被链接起来,这样如果改动了某一处的代码就不需要对整个项目重新编译

2)确定地址:使得机器代码中所调用的不确定地址可以被确定

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的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.3.1使用 readelf -a hello > helloelf2.txt把hello的elf格式信息输出到helloelf2.txt中

1)ELF头:

其中文件类型发生了变化,变为了EXEC(可执行文件)。节头部数量(Number of section headers)也发生了变化,变为了27个

2)节头:

3)符号表

多了很多其他链接文件里面的符号

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。  

  1. edb看到的虚拟地址空间:这路0x401000是init的开始地址

  1. .interp段查看上面readelf的结果可以知道是在0x400270

3).text段:

4).rodata段

5.5 链接的重定位过程分析

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

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

5.5.2使用objdump -d -r hello >hello_asm2.s

5.5.3不同点:1)在hello.o的反汇编文件中的地址都是相对于main函数基址的偏移。而在hello的反汇编文件中是虚拟地址。

2)在hello.o的反汇编文件中call和jump类的指令都是指向下一个地址。而而在hello的反汇编文件中,已经被调整为指向目标所在的虚拟地址。

5.5.4重定位:。

图1重定位表

图2在重定位前的反汇编代码

图3在重定位后的

  1. R_X86_64_PC32 相对地址引用:观察图2,我们发现第15行lea的调用就是(这里lea的作用是把提示信息的字符串赋值给rdi以打印提示信息)

下面是说明具体的工作:

在重定位的时候,链接器读出重定位表中的数据知道了①偏移量是0x18 ②要从.rodata(0x402000)开始 ③加数为-0x4就是他有四个字节的0去占位,因为pc指向下一条指令的位置,所以要把占位的加上 ④main函数从0x4010c5开始

Step1:算出lea之后的开始地址:(待填充的地址)

即main函数+offset = 0x4010c5 + 0x18=0x4010dd

Step2:算出目标字符串的位置:0x402000+0x4=0x402004

Step3:算出相对值:0x402004-0x4010dd=0xf27

  1. R_X86_64_PLT32 地址引用:观察图2,发现17行的callq调用的就是(这里callq是调用函数输出我们的字符串)

下面是说明具体的工作:

在重定位的时候,链接器读出重定位表和函数表中中①偏移量是0x1d②puts在0x 401030③main函数从0x4010c5开始④加数为-0x4

所以是0x401030-(0x4010c5+0x1d)-0x4 = 0xffff4a这是一个补码表示的负数

5.6 hello的执行流程

使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。

5.6.1首先使用-g选项生成符号表进入gdb,再在_init _start main exit四个函数处设置断点

1)_init:调用__init_misc  和 0x7ffff7df9b50 <__setfpucw>

2)_start:调用了 0x403ff0处的一个程序

  1. main :调用了 0x401030 <puts@plt> 和401070 <exit@plt>

和401040 <printf@plt>和401060 <atoi@plt>和401080 <sleep@plt> 和401050 <getchar@plt>

  1. exit  调用callq  0x7ffff7dfd7b0 <__run_exit_handlers>

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。

5.7.1使用ldd先找出那些库被动态链接了

5.7.2动态链接的概念:动态链接的概念是基于共享库建立的。在程序运行或者加载时,共享库可以加载到程序的任意内存地址并和一个程序链接起来。相较于把所有模块链接起来成为一个单独的可执行文件的静态链接,它在运行时才成为一个完整的程序,因此称为动态链接。

5.7.3使用gdb观察  info sharedlibrary

可见,在程序运行之后加载了两个共享库

5.8 本章小结

这一章讲解了链接相关的知识,首先阐述了链接的概念与作用,随后在ubuntu下实际操作并展示了链接结果,之后重定位分析链接过程、解读hello虚拟内存空间,并详细展示了动态链接的概念、作用和带来的影响。

6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念:进程是指一个执行中的程序的实例,系统中每个程序都运行在某个进程的上下文中,其内含多种程序正常执行所需要的状态。

6.1.2进程的作用:在运行一个进程时,我们的这个程序认为自己是系统当中唯一一个运行的程序,进程的作用就是提供给程序两个关键的抽象:1.独立的逻辑控制流,即程序独占使用处理器的假象。2.私有的地址空间,即程序独占使用内存系统的假象。

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

Step1:对命令行参数初步处理,分析出参数中的命令部分和参数部分。同时判断命令行是否为空

Step2:如果不为空则判断第一个命令行参数是不是一个内置的命令,如果是一个内置命令则直接执行

Step3:否则检查是否是一个应用程序。之后在搜索路径里寻找这些应用程序,如果键入的命令不是一个内部命令并且路径里没有找到这个可执行文件,则报错

Step3:如果找到可执行文件,那么创建新的进程去执行这个文件

6.3 Hello的fork进程创建过程

Shell调用fork创建子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,因此fork后子进程可以读写父进程中打开的任意文件。父进程和创建的子进程最大的区别在于其PID不同。 fork会被父进程调用一次,返回两次,父进程与创建的子进程并发执行。执行hello时,fork后的进程在前台执行,因此创建它的父进程shell暂时挂起等待hello进程执行完毕。

实际上是创建当前进程的mm_struct,vm_area_struct和页表的副本。两个进程的每个页表都标记为写时复制

6.4 Hello的execve过程

shell通过fork创建一个子进程后,execve函数在当前进程的上下文中加载并运行一个新程序即hello。 Execve需要三个参数:可执行目标文件名filename、参数列表argv、环境变量列表envp。这些都由shell构造并传递。

除非找不到filename,否则execve不会返回。(调用一次,(正常情况下)从不返回) 调用execve会将这个进程执行的原本的程序完全替换,它会删除已存在的用户区域,包括数据和代码;然后,映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的;之后映射共享区;最后把控制传递给当前的进程的程序入口。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

6.5.1逻辑控制流

 逻辑控制流是一个PC值的序列,PC值就是程序计数器的值,这些值与可执行目标文件的指令或者包含在运行时动态链接到程序的共享对象中的指令一一对应。

6.5.2时间分片

在现代计算机体系中,进程是轮流使用处理器的,每个进程都执行它的流的一部分,然后被抢占(暂时挂起),再轮到其它进程。一个逻辑流的执行在时间上与另一个流重叠被称为并发流,这两个流并发运行。 多个流并发执行的概念被称为并发。一个进程与其他进程轮流运行的概念称为多任务。一个进程执行其控制流一部分的每一个时间段叫做时间片,多任务也就被称作是时间分片。

6.5.3用户模式与内核模式

  为了保护操作系统内核,处理器在某一个控制寄存器中的一个模式位,设置模式位时,进程就运行在内核模式中,否则运行在用户模式。内核模式的代码可以无限制地访问所有处理器指令集以及全部内存和 I/O 空间。如果用户模式的进程要享有此特权,它必须通过系统调用向设备驱动程序或其他内核模式的代码发出请求。

运行程序代码初始时都是在用户模式中的,当发生中断故障或系统调用的异常时,进程从用户模式转变为内核模式。当异常发生时,控制传递到异常处理程序,处理器将模式转变为内核模式。内核处理程序运行在内核模式中,当它返回到应用程序代码时,处理器把模式从内核模式改回到用户模式。

6.5.4进程上下文切换

  上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这样的决策叫做调度,由内核中的调度器的代码处理。在这个抢占过程中需要用到上下文切换,上下文切换保存当前进程的上下文,恢复先前某个被抢占的上下文,并将控制传递给新恢复的进程。

6.6 hello的异常与信号处理

 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

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

6.6.1异常种类

      1中断:来自I/O设备的异常

2陷阱:有意的异常

3故障:存在可以恢复的错误

4终止:不可恢复的错误

6.6.2信号类型

 1sigint:ctrl+c用于中断程序 2sigtstp:ctrl+z用于暂停程序

3sigchld:子程序终止时向父进程发送的指令

6.6.3具体的

1乱按键盘

2ctrlc和ctrlz

3ps和jobs

4pstree

5 fg和kill
      

6.7 本章小结

本章概述了hello进程大致的执行过程,阐述了进程、shell、fork、execve等相关概念,之后从逻辑控制流、时间分片、用户模式/内核模式、上下文切换等角度详细分析了进程的执行过程。并在运行时尝试了不同形式的命令和异常,每种信号都有不同处理机制,针对不同的shell命令,hello会产生不同响应

7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

7.1.1逻辑地址

 程序代码经过编译后出现在汇编程序中地址。逻辑地址由选择符(在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏移部分)组成,是一个相对值。

7.1.2线性地址

  线性地址空间是一个非负整数的集合。逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。

7.1.3虚拟地址

  虚拟地址空间是0到N的所有整数的集合(N是正整数),是线性地址空间的有限子集。分页机制以虚拟地址为桥梁,将硬盘和物理内存联系起来。是程序自己分配的。

7.1.4物理地址

   CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

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

在 Intel 平台下,逻辑地址是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。

CS寄存器(代码段寄存器): CS寄存器存储了当前执行的指令所在的代码段的起始地址。它是一个16位寄存器,指示了代码在内存中的位置。CS寄存器的值与代码段的段基址相关,形成了代码段的起始物理地址。

EIP寄存器:用来存储CPU要读取指令的地址,CPU通过EIP寄存器读取即将要执行的指令。每次CPU执行完相应的汇编指令之后,EIP寄存器的值就会增加。

如果用 selector 去 GDT( Global Descriptor Table,全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address(线性地址)。这个过程就称作段式内存管理。

逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符),可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

全局的段描述符,放在GDT中,一些局部的段描述符,放在“LDT(Local Descriptor Table,局部段描述符表)”中。

给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。再由基地址加上偏移量的值,便得到了线性地址。

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

线性地址(VA)到物理地址(PA)之间的转换通过分页机制完成。分页机制类似主存和Cache之间的分块机制,分页机制对虚拟地址和物理内存进行分页,页的大小通常是4KB到2M。在x86-64机器上,虚拟地址空间的N是2的48次方,有256TB,比正常的硬盘大得多

分页机制中使用一个叫做页表的数据结构来记录这些关系,页表也是存储在内存中的,是由操作系统维护的。每个进程都有一个页表,页表中的每一项,即PTE(页表条目),记录着该对应的虚拟地址空间的那一页是否有效(即是否有对应的物理内存上的页),物理页的起始位置或磁盘地址,访问权限等信息。PTE根据不同的映射状态也被划分为三种状态:未分配、未缓存、已缓存

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

7.4.1页表

      页表是 PTE(页表条目)的数组,它将虚拟页映射到物理页,每个 PTE 都有一个有效位和一个 n 位地址字段,有效位表明该虚拟页是否被缓存在 DRAM 中。虚拟地址分为两个部分,虚拟页号(VPN,Virtual Page Number)和虚拟页面偏移量(VPO,Virtual Page Offset)。其中VPN需要在PTE中查询对应,而VPO则直接对应物理地址偏移(PPO)。

7.4.2TLB

   TLB(translation lookaside buffer,地址转换后备缓冲器,习惯称之为“快表”)是一个位于MMU(Memory Management Unit,内存管理单元)中,关于PTE的一个缓存。TLB是一个小的、虚拟寻址的缓存,其中每一行均保存了一个由单个PTE组成的块。TLB有高度的相联性,能够加速地址翻译,而多级页表能够对页表进行压缩,便于大量存储。

7.4.3变换机制

在从VA翻译得到PA的过程中,MMU首先用VPN向TLB申请请求对应的PTE,如果命中,那么直接跳过后面的步骤;之后MMU生成PTE地址,从高速主存请求得到PTE,高速缓存或主存会向MMU返回PTE。若PTE有效位为0,说明缺页,MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页(若页面修改,则换出到磁盘)。之后缺页处理程序调入新的页面,并更新PTE。之后却也处理程序返回原进程,并重新执行导致缺页的指令。

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

通过内存地址的组索引获得值,如果对应的值是data则像L1 d-cache对应组中查找,如果是指令,则向L1 i-cache对应组中查找。将L1对应组中的每一行的标记位进行对比,如果相同并且有效位为1则命中,获得偏移量,取出相应字节,否则不命中,向下一级cache寻找,直到向内存中寻找。

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

step1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。

step2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。

step 3.映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

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

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

7.8.1缺页故障

      一个虚拟页没被缓存在DRAM中,即DRAM缓存不命中被称为缺页。当CPU引用了一个页表条目中的一个字,而该页表条目并未被缓存在DRAM中,地址翻译硬件从内存中读取该页表条目,从有效位为0可以判断尚未被缓存,进而触发缺页异常。

7.8.2缺页中断处理

  缺页异常调用缺页异常处理程序,该程序会选择一个牺牲页,如果这个牺牲页在DRAM中已被修改,那么就将他写回磁盘,之后将引用的虚拟页复制到内存中的原来牺牲页所在位置,并对页表条目进行更新,随后返回。当异常处理程序返回时,它会重新启动缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。此时需要调用的虚拟页已经缓存到主存中了,则页命中可以由地址翻译硬件正常处理。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

7.9.1动态内存分配的概念

    在程序运行时程序员使用动态内存分配器,例如调用malloc函数从而获得虚拟内存。分配器将堆(heap)视为一组不同大小的块(blocks)的集合来维护。每个块要么是已分配的,要么是空闲的。

7.9.2堆

    由动态存储分配管理器维护着的进程虚拟内存区域称为堆。当内存中的碎片和垃圾被回收之后,内存中就会产生多余的空闲空间。为了避免内存空间的浪费,需要记录这些空闲块,而采用隐式空闲链表和显式空闲链表的方法则可以实现这一操作。

7.9.3隐式空闲链表

     当隐式空闲链表工作时,若分配块比空闲块小,则还可以把空闲块分为两部分,一部分用来承装分配块从而避免可能导致的浪费。

隐式链表采用边界标记的方法进行双向合并。即脚部与头部是均为 4 个字节,这一部分用来存储块的大小并表明这个块的空闲与分配状态。同时定位头部和尾部,可以以常数时间来进行块的合并。由此,无论是与下一块还是与上一块合并,都可以通过他们的头部或尾部得知块大小,从而定位整个块,避免了从头遍历链表导致的时间花销极高。

7.9.4 显示空闲链表

  与隐式空闲链表不同,显式空闲链表只记录空闲块而不记录所有块。

显示空闲链表的思路是维护多个空闲链表,每个链表中的块有大致相等的大小,分配器维护着一个空闲链表数组,每个大小类一个空闲链表。当需要分配块时,只需要在对应的空闲链表中搜索即可。

7.10本章小结

这一章聚焦于hello程序在计算机系统当中的存储与管理问题,并引出计算机系统当中的一个重要概念“虚拟内存”。在本章当中,我们详细讲述了hello的四种地址及其寻找、变换、翻译的过程,阐释了TLB相关概念和流程,三级缓存、动态内存管理的要点重点。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.1.1设备的模型化:文件

 一个Linux文件就是一个m个字节的序列,所有的I/O设备都被模型化为文件,所有的输入输出都被当作是文件的读和写来执行。。

8.1.2设备管理:unix io接口

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

8.2 简述Unix IO接口及其函数

8.2.1 UNIX接口

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

(2)I/O设备:内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

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

8.2.2 unix接口相关的函数

(1)int open(char *filename, int flags, mode_t mode);

     进程通过调用open函数打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:O_RDONLY:只读、O_WRONLY:只写和O_RDWR可读可写。mode参数指定了新文件的访问权限位。

(2)int close(fd):

     进程调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符

(3)ssize_t read(int fd, void *buf, size_t n);

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

(4)ssize_t write(int fd, const void *buf, size_t n);

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

8.3 printf的实现分析

由上图可知printf用了两个外部函数,一个是vsprintf,还有一个是write。

 图:vsprintf 的实现

vsprintf函数作用是接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。write函数将buf中的i个元素写到终端。在unistd.h头文件中,我们可以找到write函数的声明:

write()函数有三个参数:

fd: 文件描述符,标识待写入的文件或者套接字。

buf: 指向要写入的数据的缓冲区。

count: 要写入的字节数。

返回值为实际写入的字节数,错误时返回-1,并设置errno。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。

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

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

8.4 getchar的实现分析

getchar函数调用了read函数,通过系统调用read读取存储在键盘缓冲区的ASCII码,直到读到回车符才返回。不过read函数每次会把所有内容读进缓冲区,如果缓冲区本来非空,则不会调用read函数,而是简单的返回缓冲区最前面的元素。

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

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

8.5本章小结

本章主要介绍了hello的I/O管理机制,先简述了I/O设备被抽象为文件的现象,随后介绍了I/O的设备管理方法——unix IO接口,随后对unixIO接口做了介绍之后,给出了Unix IO接口的相关函数,并在此基础上,对printf和getchar的实现原理进行了介绍。

结论

hello从出生(编写完成)到死亡(进程被回收)总共经历了如下几个步骤:

1、预处理(cpp)。将hello.c进行预处理(处理所有的#后面的代码),将文件调用的所有外部库文件合并展开,生成一个经过修改的hello.i文件。

2、编译(ccl)。将hello.i文件翻译成为一个包含汇编语言的文件hello.s。

3、汇编(as)。将hello.s翻译成为一个可重定位目标文件hello.o。这时候不再是文本文件了。

4、链接(ld)。将hello.o文件和可重定位目标文件和动态链接库链接起来,生成一个可执行目标文件hello。

5、运行。在shel1中输入./hello 2023110264 谢辰阳 13868460946 并回车。

6、创建进程。首先终端判断出输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。

7、加载程序。shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。

8、执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。

9、访问内存:MU将程序中使用的虚拟内存地址通过页表映射成物理地址。

10、信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。

11、终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。

附件

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

  1. hello.i: hello.c预处理后的文件。 用于处理#对应的内容

2)hello.s: hello.i编译后的文件。由编译器生成的汇编代码

3)hello.o: hello.s汇编后的文件

4)helloelf.txt:readelf产生的hello.o的elf分析文件

5) hello_asm1.s:使用objdump生成的hello.o的反汇编文件

6)helloelf2.txt:hello的elf文件

7)hello_asm2.s:使用objdump生成的hello的反汇编文件

8)hello:最终编译生成的可执行文件

9)hello.c:程序源文件

参考文献

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

[2] R. E. Bryant, D. R. O'Hallaron, and T. Krentel, Computer systems: A programmer's perspective, 3rd ed. Pearson, 2015.  深入了解计算机系统

[3] [转]printf 函数实现的深入剖析 - Pianistx - 博客园 printf函数实现的深入了解与分析

[4] 【Linux】GDB保姆级调试指南(什么是GDB?GDB如何使用?)_gdb教程-CSDN博客 GDB保姆式指南

转载自CSDN-专业IT技术社区

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/m0_62499723/article/details/144851613

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--