计算机系统

大作业

计算机科学与技术学院

202211

本文基于《深入理解计算机系统》,通过对一个简单的程序Hello在Linux环境下的生命周期的分析,论述其从hello.c经过预处理、编译、汇编、链接等一系列操作生成可执行文件hello,再通过程序对进程的管理、内存空间的分配、信号和异常的处理、对 I/O 设备的调用等环节彻底解释hello从创建到结束的过程,进而加深对计算机系统的理解。

关键词:计算机系统;程序的生命周期;hello的一生;程序编译过程

第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简介

1.1.1 P2P: From Program to Process

P2P,即从程序到进程。用户利用高级语言C语言编写hello.c源程序,经过cpp预处理形成hello.i,再经过ccl编译形成汇编语言程序hello.s,然后经过as转换为机器语言指令,形成可重定位目标程序hello.o,最后通过ld与库函数链接并符号解析与重定位,形成可执行目标文件hello。而后可执行文件hello通过shell加载,fork产生子进程,经过以上步骤hello程序(program)变成了hello进程(process)。

1.1.2 020: From Zero-0 to Zero -0

020,即从运行到结束。初始时内存中没有hello文件相关的内容,通过fork产生hello子进程后,通过execve进行加载,先删除当前虚拟地址已存在的数据结构,为hello的代码、数据、bss等创建区域,然后映射共享区域,设置程序计数器,进入main函数,CPU分配时间片执行逻辑控制流。执行过程中,虚拟内存为进程提供独立的空间;存储结构层层递进,让数据从磁盘传输到CPU中;TLB、分级页表等也为数据的高效访问提供保障;I/O设备通过描述符与接口实现了hello的输入输出。多方面合作配合之下,hello完成执行。然后,shell回收hello进程,删除hello的所有痕迹,释放运行中占用的内存空间。至此,hello从运行到结束,完成020过程。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2GHz;2G RAM;256GHD Disk

1.2.2 软件环境

Windows 10 64位;Vmware-workstation-16;Ubuntu 20.04 LTS 64位

1.2.3 开发与调试工具

Codeblocks 64位;vi/vim/gedit+gcc

1.3 中间结果

表1.3-1 中间结果

文件名

功能

hello.c

源代码

hello.i

预处理后的文本文件

hello.s

编译后的汇编文件

hello.o

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

hello

链接后的可执行文件

hello.elf

用readelf读取hello.o的ELF格式信息

hello.asm

反汇编hello.o的反汇编文件

hello2.elf

由hello可执行文件生成的.elf文件

hello2.asm

反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

本章主要介绍了hello的P2P和020过程,从总体上简要阐述了hello的一生,给出了论文研究时的环境与工具以及中间生成的文件信息。

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

预处理步骤是指程序开始运行时,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序的过程。

2.1.2 预处理的作用

  1. 宏展开:将所有的宏定义进行替换,然后删除#define
  2. 条件编译:如果源代码中包含条件预处理指令(如#if),就会先判断条件,再修改源代码
  3. 头文件展开:对文件包含命令#include,引入对应头文件,将头文件的内容(.h)插入到命令所在位置,从而把头文件和当前源文件连接成一个源文。

2.2在Ubuntu下预处理的命令

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

图2.2-1 预处理命令截图

2.3 Hello的预处理结果解析

预处理后的源文件从23行扩展到了3060行,main函数在第3047行,预处理器将头文件中的内容引入hello.i,将需要用到的库函数等加入到了文本中,让程序能够继续被编译器编译。

  1. 包含文件信息

    图2.3-1 hello.i中部分文件信息

    1. 类型定义信息

    图2.3-2 hello.i中部分类型定义信息

    1. 函数声明信息

    图2.3-3 hello.i中部分函数声明信息

    1. 源码部分

    图2.3-4 hello.i中源码部分

    2.4 本章小结

    在本章中,我们对hello.c进行了预处理,生成hello.i,预处理器会进行宏展开、头文件展开、条件编译等处理,并删除注释,对函数源码并不做过多修改,hello.i文件可用于下一步的处理。

    第3章 编译

    3.1 编译的概念与作用

    3.1.1 编译的概念

    编译是指预处理后,编译器(ccl)将预处理文件hello.i翻译成汇编语言文件hello.s。

    3.1.2 编译的作用

    1. 语法检查:检查代码是否存在语法错误,如果有错误的话就会报错。
    2. 生成汇编代码:将程序翻译成汇编语言,从而在下一阶段可以让汇编器翻译成机器语言指令。
    3. 代码优化:编译器会对程序进行优化,生成效率更高的目标代码。

    3.2 在Ubuntu下编译的命令

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

  2. 图3.2-1 编译命令

    3.3 Hello的编译结果解析

    3.3.1 汇编代码展示

    1. .file“hello.c”
    2. .text
    3. .section.rodata
    4. .align8
    5. .LC0:
    6. .string“\347\224\250\346\263\225:Hello\345\255\246\345\217\267\345\247\223\345\220\215\347\247\222\346\225\260\357\274\201”
    7. .LC1:
    8. .string“Hello%s%s\n”
    9. .text
    10. .globlmain
    11. .typemain,@function
    12. main:
    13. .LFB6:
    14. .cfi_startproc
    15. endbr64
    16. pushq%rbp
    17. .cfi_def_cfa_offset16
    18. .cfi_offset6,-16
    19. movq%rsp,%rbp
    20. .cfi_def_cfa_register6
    21. subq$32,%rsp
    22. movl%edi,-20(%rbp)
    23. movq%rsi,-32(%rbp)
    24. cmpl$4,-20(%rbp)
    25. je.L2
    26. leaq.LC0(%rip),%rdi
    27. callputs@PLT
    28. movl$1,%edi
    29. callexit@PLT
    30. .L2:
    31. movl$0,-4(%rbp)
    32. jmp.L3
    33. .L4:
    34. movq-32(%rbp),%rax
    35. addq$16,%rax
    36. movq(%rax),%rdx
    37. movq-32(%rbp),%rax
    38. addq$8,%rax
    39. movq(%rax),%rax
    40. movq%rax,%rsi
    41. leaq.LC1(%rip),%rdi
    42. movl$0,%eax
    43. callprintf@PLT
    44. movq-32(%rbp),%rax
    45. addq$24,%rax
    46. movq(%rax),%rax
    47. movq%rax,%rdi
    48. callatoi@PLT
    49. movl%eax,%edi
    50. callsleep@PLT
    51. addl$1,-4(%rbp)
    52. .L3:
    53. cmpl$8,-4(%rbp)
    54. jle.L4
    55. callgetchar@PLT
    56. movl$0,%eax
    57. leave
    58. .cfi_def_cfa7,8
    59. ret
    60. .cfi_endproc
    61. .LFE6:
    62. .sizemain,.-main
    63. .ident“GCC:(Ubuntu9.4.0-1ubuntu1~20.04.1)9.4.0”
    64. .section.note.GNU-stack,“”,@progbits
    65. .section.note.gnu.property,“a”
    66. .align8
    67. .long1f-0f
    68. .long4f-1f
    69. .long5
    70. 0:
    71. .string“GNU”
    72. 1:
    73. .align8
    74. .long0xc0000002
    75. .long3f-2f
    76. 2:
    77. .long0x3
    78. 3:
    79. .align8
    80. 4:

    3.3.2 汇编文本结构分析

    表3.2.2-1 hello.s文件结构

    内容

    含义

    .file

    源文件

    .text

    代码段

    .global

    全局变量

    .data

    存放已经初始化的全局和静态C 变量

    .section .rodata

    存放只读变量

    .align

    对齐方式

    .type

    表示是函数类型/对象类型

    .size

    表示大小

    .long .string

    表示是long类型/string类型

    3.3.3 常量

    编译时对常量进行编码,并将其存储在只读代码区的 .rodata节,在程序运行时会直接通过寻址找到常量。

    例如将hello.c中“Usage: Hello 学号 姓名 秒数!”编译为汇编代码第6行:

    1. .string“\347\224\250\346\263\225:Hello\345\255\246\345\217\267\345\247\223\345\220\215\347\247\222\346\225\260\357\274\201”

    3.3.4 变量

    不同类型的变量在不同位置定义,初始化的全局变量和静态变量定义在只读代码区的.bss节,已初始化的全局和静态变量定义在只读代码区的.data节,局部变量在堆上进行定义和释放。

    例如,局部变量int i被保存在栈上,通过机器指令对其赋值

    1. movl$0,-4(%rbp)

    3.3.5 赋值操作

    对局部变量进行赋值操作,使用MOV指令,根据不同的数据大小选择不同指令movb、movw、movl、movq等。具体见3.3.4例。

    3.3.5 算术运算

    表3.3.5-1 算数指令

    指令

    效果

    leaq s,d

    d=&s

    inc d

    d+=1

    dec d

    d-=1

    neg d

    d=-d

    add s,d

    d=d+s

    sub s,d

    d=d-s

    imulq s

    r[%rdx]:r[%rax]=s*r[%rax]

    mulq s

    r[%rdx]:r[%rax]=s*r[%rax]

    idivq s

    r[%rdx]=r[%rdx]:r[%rax] mod s

    r[%rax]=r[%rdx]:r[%rax] div s

    divq s

    r[%rdx]=r[%rdx]:r[%rax] mod s

    r[%rax]=r[%rdx]:r[%rax] div s

    在hello.s中,例如,实现i++的操作:

    1. addl$1,-4(%rbp)

    开辟栈以及回收栈:

    1. subq$32,%rsp

    3.3.6 比较和跳转操作

    通过COM指令进行比较,计算两个值相减大小,根据结果设置条件码,根据条件码来判断跳转值,也可通过跳转指令J判断有无符号。

    例如,检查argc是否不等于4。在hello.s中,使用cmpl $4,-20(%rbp),比较 argc与4的大小并设置条件码,为下一步je利用条件码进行跳转作准备。

    1. cmpl$4,-20(%rbp)
    2. je.L2

    表3.3.6-1 跳转指令

    指令

    条件

    jmp

    直接跳转

    je

    相等

    Jne

    不等于

    Js

    小于

    Jns

    小于等于

    Jg

    大于

    Jge

    大于等于

    Ja

    大于(无符号)

    Jae

    大于等于(无符号)

    jbe

    小于等于(无符号)

    3.3.7 数组/指针操作

    对数组的索引相当于在第一个元素地址的基础上通过加索引值乘以数据大小来实现。

    例如,在hello.c中,存在char *argv[],根据图3-3可知,根据argv首地址获得argv[1]和argv[2]需要通过加减操作:

    1. movq-32(%rbp),%rax
    2. addq$16,%rax
    3. movq(%rax),%rdx
    4. movq-32(%rbp),%rax
    5. addq$8,%rax
    6. movq(%rax),%rax
    7. movq%rax,%rsi

    3.3.8 函数操作

    hello.c中包括main函数,printf函数,sleep函数,getchar函数,exit函数。

    首先,内核shell获取命令行参数和环境变量地址,执行main函数,在main中需要调用其它函数,在main中为被调用函数分配栈空间。调用函数需要借助栈,先将返回地址压入栈中,并将PC设为被调用函数的起始地址,然后调用。返回时,先从栈中弹出返回地址,再PC设置为该返回地址。return正常返回后,leave恢复栈空间。

    在hello.s中调用函数有:

    callputs@PLT

    callexit@PLT

    callprintf@PLT

    callsleep@PLT

    callgetchar@PLT

    3.4 本章小结

    本章主要探讨编译器将经过预处理阶段后的C程序hello.i翻译成汇编语言程序的处理过程,包括对数据、算术操作、关系操作、控制转移、数组操作、函数操作的处理。编译器也会在处理过程中对程序进行一些优化,最终的结果被保存在hello.s文件中,能够在下一阶段让汇编器翻译机器语言指令。

    第4章 汇编

    4.1 汇编的概念与作用

    4.1.1 汇编的概念

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

    4.1.2 汇编的作用

    将汇编指令转换成机器可以直接读取分析的机器指令,生成hello.o文件,用于后续的链接。

    4.2 在Ubuntu下汇编的命令

    汇编命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

  3. 图4.2-1 汇编命令

    4.3 可重定位目标elf格式

    在shell中输入readelf -a hello.o > hello.elf指令获得hello.o文件的 ELF 格式:

  4. 图4.3-1 生成elf格式文件

    4.3.1 ELF头

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

  5. 图4.3.1-1 ELF头

    4.3.2 节头

    节头记录了各节名称及大小、类型及全体大小、地址及旗标、连接、信息和偏移量及对齐信息。

  6. 图4.3.2-1节头

    4.3.3 重定位节

    当链接器把这个目标文件和其他文件组合时,需要修改表中的这些位置。一般,调用外部函数或者引用全局变量的指令都需要修改。

    其中包括R_X86_64_PC32( PC相对地址的引用)和R_X86_64_32(绝对地址的引用)。

    图4.3.3-1重定位头

    4.3.4 符号表

    符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

  7. 图4.3.4-1符号表

    4.4 Hello.o的结果解析

    使用objdump -d -r hello.o > hello.asm分析hello.o的反汇编,并与第3章的 hello.s文件进行对照分析。

  8. 图4.4-1生成反汇编文件

    4.4.1 反汇编代码

    1. hello.o文件格式elf64-x86-64
    2. Disassemblyofsection.text:
    3. 0000000000000000
      :
    4. 0:f30f1efaendbr64
    5. 4:55push%rbp
    6. 5:4889e5mov%rsp,%rbp
    7. 8:4883ec20sub$0x20,%rsp
    8. c:897decmov%edi,-0x14(%rbp)
    9. f:488975e0mov%rsi,-0x20(%rbp)
    10. 13:837dec04cmpl$0x4,-0x14(%rbp)
    11. 17:7416je2f
    12. 19:488d3d00000000lea0x0(%rip),%rdi#20
    13. 1c:R_X86_64_PC32.rodata-0x4
    14. 20:e800000000callq25
    15. 21:R_X86_64_PLT32puts-0x4
    16. 25:bf01000000mov$0x1,%edi
    17. 2a:e800000000callq2f
    18. 2b:R_X86_64_PLT32exit-0x4
    19. 2f:c745fc00000000movl$0x0,-0x4(%rbp)
    20. 36:eb48jmp80
    21. 38:488b45e0mov-0x20(%rbp),%rax
    22. 3c:4883c010add$0x10,%rax
    23. 40:488b10mov(%rax),%rdx
    24. 43:488b45e0mov-0x20(%rbp),%rax
    25. 47:4883c008add$0x8,%rax
    26. 4b:488b00mov(%rax),%rax
    27. 4e:4889c6mov%rax,%rsi
    28. 51:488d3d00000000lea0x0(%rip),%rdi#58
    29. 54:R_X86_64_PC32.rodata+0x22
    30. 58:b800000000mov$0x0,%eax
    31. 5d:e800000000callq62
    32. 5e:R_X86_64_PLT32printf-0x4
    33. 62:488b45e0mov-0x20(%rbp),%rax
    34. 66:4883c018add$0x18,%rax
    35. 6a:488b00mov(%rax),%rax
    36. 6d:4889c7mov%rax,%rdi
    37. 70:e800000000callq75
    38. 71:R_X86_64_PLT32atoi-0x4
    39. 75:89c7mov%eax,%edi
    40. 77:e800000000callq7c
    41. 78:R_X86_64_PLT32sleep-0x4
    42. 7c:8345fc01addl$0x1,-0x4(%rbp)
    43. 80:837dfc08cmpl$0x8,-0x4(%rbp)
    44. 84:7eb2jle38
    45. 86:e800000000callq8b
    46. 87:R_X86_64_PLT32getchar-0x4
    47. 8b:b800000000mov$0x0,%eax
    48. 90:c9leaveq
    49. 91:c3retq

    4.4.2 与汇编代码比较

    将hello.asm和hello.s进行比较,大部分相同,主要有一下几个方面不同:

    1. 包含内容:

    hello.s中包含.type .size .align以及.rodata只读数据段等信息,而hello.asm中只有函数的相关内容。

    2.分支转移:

    在hello.s中,跳转指令的目标地址直接记为段名称,如.L1,.L2等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址。

    3.函数调用:

    在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。

    4. 全局变量访问:

    在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip进行访问。

    4.4.3 机器语言的构成

    机器语言是机器能直接识别的程序语言或指令代码,无需经过翻译,每一操作码在计算机内部都有相应的电路来完成它,或指不经翻译即可为机器直接理解和接受的程序语言或指令代码。机器语言使用绝对地址和绝对操作码。不同的计算机都有各自的机器语言,即指令系统。从使用的角度看,机器语言是最低级的语言。

    4.4.4 机器语言与汇编语言的映射关系

    汇编语言和机器语言一般是一一对应的,汇编语言是机器语言的符号表示方式。而不同类型的CPU 有不同的机器指令系统,也就有不同的汇编语言,所以,汇编语言程序与机器有着密切的关系。所以,除了同系列、不同型号CPU 之间的汇编语言程序有一定程度的可移植性之外,其它不同类型(如:小型机和微机等)CPU 之间的汇编语言程序是无法移植的,也就是说,汇编语言程序的通用性和可移植性要比高级语言程序低。

    4.5 本章小结

    本章讨论了汇编阶段将汇编代码hello.s翻译成机器语言指令hello,o的过程,等待下一步链接器的处理。同时,比较了汇编代码与反汇编代码之间的不同之处。

    第5章 链接

    5.1 链接的概念与作用

    5.1.1 链接的概念

    链接是指链接器(ld)将各种代码和数据片断收集并组合成一个单一可执行目标文件的过程。

    5.1.2 链接的作用

    使得分离编译成为可能,我们可以独立的修改和编译模块,当我们改变这些模块的其中一个时,只需简单的重新编译它,并重新链接应用,而不必重新编译其他文件。

    5.2 在Ubuntu下链接的命令

    链接命令:ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello

  9. 图5.2-1 链接命令

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

    在Shell中输入命令 readelf -a hello > hello2.elf 生成 hello 程序的 ELF 格式文件,保存为hello2.elf:

  10. 图5.3-1 生成hello.o的elf格式

    5.3.1 ELF头

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

  11. 图5.3.1-1 ELF头

    5.3.2 节头

    节头记录了各节名称及大小、类型及全体大小、地址及旗标、连接、信息和偏移量及对齐信息。

  12. 图5.3.2-1节头

    5.3.3 程序头

    一个结构数组,描述了系统准备程序执行所需的段或其他信息。

  13. 图5.3.3-1程序头

    5.3.4 Dynamic section(动态section)

    图5.3.4-1 Dynamic section

    5.3.5 符号表

    符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

  14. 图5.3.5-1符号表

    5.4 hello的虚拟地址空间

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

    根据计算机系统的特性,程序被载入至地址0x400000~0x401000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。通过ELF可知,程序从0x00400000到0x00400fff,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。

  15. 图5.4-1用edb查看虚拟地址空间

    5.5 链接的重定位过程分析

    在Shell中使用命令objdump -d -r hello > hello2.asm生成反汇编文件hello2.asm,与第四章中生成的hello.o.asm文件进行比较:

  16. 图5.5-1生成反汇编文件

    5.5.1 重定位概念

    链接器在完成符号解析以后,就把代码中的每个符号引用和一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。

    然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在 hello 到 hello.o 中,首先是重定位节和符号定义,链接器将所有输入到 hello 中相同类型的节合并为同一类型的新的聚合节。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改 hello 中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

    5.5.2 hello与hello.o的不同

    1.链接后函数数量增加,hello中增加了外部函数。

    2.函数调用指令call的参数发生变化,hello中调用为call+函数名,hello.o中为call+相对偏移地址。

    3.跳转指令参数发生变化。

    4.hello.o中是相对偏移地址,hello为虚拟内存地址。

    5.6 hello的执行流程

    hello在执行的过程中要执行载入、执行和退出三个过程,列出其调用与跳转的各个子程序名或程序地址。

    表5.6-1 程序名称与程序地址

    程序名称

    程序地址

    4010f0

    4011c0

    401000

    401125

    401020

    401090

    4010a0

    4010c0

    4010d0

    4010e0

    4010b0

    401230

    401238

    5.7 Hello的动态链接分析

    延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

    PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

    GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

  17. 图5.7-1调用 dl_init 之前的全局偏移表

    图5.7-2调用 dl_init 之后的全局偏移表

    由上图对比可知,调用dl_init函数后,发生动态链接,GOT条目改变。

    5.8 本章小结

    本章介绍了链接的概念和功能,分析可执行文件hello的ELF格式及其虚拟地址空间,并对重定位、动态链接进行深入的分析。

    第6章 hello进程管理

    6.1 进程的概念与作用

    6.1.1 进程的概念

    进程的经典定义就是一个执行中的程序的实例。进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

    6.1.2 进程的作用

    每次用户通过向 shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

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

    6.2.1 Shell作用

    读取输入并解析命令行;替换特别字符,比如通配符和历史命令符;设置管道、重定向和后台处理;处理信号;程式执行的相关设置。

    1. 可交互,和非交互的使用shell。在交互式模式,shell从键盘接收输入;在非交互式模式,shell从文件中获取输入。

    2. shell中可以同步和异步的执行命令。在同步模式,shell要等命令执行完,才能接收下面的输入。在异步模式,命令运行的同时,shell就可接收其它的输入。重定向功能,可以更细致的控制命令的输入输出。另外,shell允许设置命令的运行环境。

    3. shell提供了少量的内置命令,以便自身功能更加完备和高效。

    4. shell除了执行命令,还提供了变量,流程控制,引用和函数等,类似高级语言一样,能编写功能丰富的程序。

    5. shell强大的的交互性除了可编程,还体现在作业控制,命令行编辑,历史命令,和别名等方面。

    6.2.2 Shell 处理流程

    命令行是一串 ASCII 字符由空格分隔。字符串的第一个单词是一个可执行程序,或者是 shell 的内置命令。命令行的其余部分是命令的参数。

    如果第一个单词是内置命令,shell 会立即在当前进程中执行。否则,shell 会新建一个子进程,然后再子进程中执行程序。新建的子进程又叫做作业。通常,作业可以由 Unix 管道连接的多个子进程组成。

    如果命令行以&符号结尾,那么作业将在后台运行,这意味着在打印提示符并等待下一个命令之前,shell 不会等待作业终止。否则,作业在前台运行,这意味着 shell 在作业终止前不会执行下一条命令行。 因此,在任何时候,最多可以在一个作业中运行在前台。 但是,任意数量的作业可以在后台运行。例如,键入命令行:sh> jobs,会让 shell 运行内置命令 jobs。键入命令行 sh> /bin/ls -l -d 会导致 shell 在前台运行 ls 程序。根据约定,shell会执行程序的 main 函数 int main(int argc, char *argv[]),argc 和 argv 会接收到下面的值:

    argc == 3,

    argv[0] == ‘‘/bin/ls’’,

    argv[1]== ‘‘-l’’,

    argv[2]== ‘‘-d’’.

    下面以&结尾的命令行会在后台执行 ls 程序:

    sh> /bin/ls -l -d &

    Unix shell 支持作业控制的概念,允许用户在前台和后台之间来回移动作业,并更改进程的状态(运行,停止或终止)。在作业运行时,键入 ctrl-c会将 SIGINT 信号传递到前台作业中的每个进程。SIGINT 的默认动作是终止进程。类似地,键入 ctrl-z 会导致 SIGTSTP 信号传递给所有前台进程。SIGTSTP 的默认操作是停止进程,直到它被 SIGCONT 信号唤醒为止。Unixshell 还提供支持作业控制的各种内置命令。例如:

    jobs:列出运行和停止的后台作业。

    bg :将停止的后台作业更改为正在运行的后台作业。

    fg :将停止或运行的后台作业更改为在前台运行。

    kill :终止作业。

    6.3 Hello的fork进程创建过程

    父进程通过调用fork函数创建一个新的、处于运行状态的子进程。

    函数原型:int fork(void);

    调用fork函数后,子进程返回0,父进程返回子进程的PID;新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本;子进程获得与父进程任何打开文件描述符相同的副本;但是子进程有不同于父进程的PID。fork函数:被调用一次,却返回两次!

    Fork具体处理hello文件过程:

    首先,带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。

  18. 图6.3-1程序执行过程

    6.4 Hello的execve过程

    调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。

    6.5 Hello的进程执行

    6.5.1 进程时间片

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

    6.5.2 上下文信息

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

    6.5.3 用户模式和内核模式

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

    6.5.4 执行过程

    进程调度指在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。内存收到中断信号之后,将当前进程加入等待序列,进行上下文切换将当前的进程控制权交给其他进程,当再次收到中断信号时将hello从等待队列加入运行队列。

    6.6 hello的异常与信号处理

    异常可以分为四类:中断(interrupt),陷阱(trap),故障(fault)和终止(abort)。

    中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。在当前指令完成执行后,处理器注意到中断引脚的电压变高,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回后,它就将控制返回给下一条指令。

    陷阱是有意的异常,是执行一条指令的结果。应用程序执行一次系统调用,然后把控制传递给处理程序,陷阱处理程序运行后,返回到syscall之后的指令。

    故障由错误情况引起,故障发生时处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。

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

    以下给出程序运行过程中按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令的运行结果:

    图6.6-1 乱按键盘

  19. 图6.6-2 执行Ctrl-c

  20. 图6.6-3 执行Ctrl-z以及ps、jobs

    图6.6-4 执行pstree

  21. 图6.6-5 执行kill

    图6.6-6 执行fg返回前台

    6.7本章小结

    本章主要讨论了进程和shell的概念与作用,进程的创建和执行过程,以及对异常和信号的处理。

    第7章 hello的存储管理

    7.1 hello的存储器地址空间

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

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

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

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

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

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

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

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

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

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

    7.6 hello进程fork时的内存映射

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

    7.7 hello进程execve时的内存映射

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

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

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

    7.9动态存储分配管理

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

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

    7.10本章小结

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

    (第7 2分)

    第8章 hello的IO管理

    8.1 Linux的IO设备管理方法

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

    设备的模型化:文件

    设备管理:unix io接口

    8.2 简述Unix IO接口及其函数

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

    8.3 printf的实现分析

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

    [转]printf 函数实现的深入剖析 – Pianistx – 博客园

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

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

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

    8.4 getchar的实现分析

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

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

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

    8.5本章小结

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

    (第81分)

    结论

    hello的一生,从预处理、编译、汇编到链接,生成可执行文件,再到内存加载运行,又从内存回收,结束生命。它经历的过程包括:

    1. hello.c诞生

    程序员通过高级语言编写hello.c程序,存储在内存中;

    1. 预处理

    将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;

    1. 编译

    通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;

    1. 汇编

    将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;

    1. 链接

    通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成可执行的目标文件hello;

    1. 加载运行

    打开Shell,在其中键入 ./hello 2021112888 lbz 2,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;

    1. 执行指令

    在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;

    1. 访存

    内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;

    1. 动态申请内存

    printf 会调用malloc 向动态内存分配器申请堆中的内存;

    1. 信号处理

    进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;

    1. 终止并被回收

    Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。

    感悟:

    在这次大作业中,通过一个小小的程序HELLO,让我形象地看到了一个程序从诞生,到运行,到完成使命离开的全过程,更好地理解了计算机系统原理,并对计算机底层产生了深厚的兴趣,想要更进一步地学习。

    附件

    文件名

    功能

    hello.c

    源代码

    hello.i

    预处理后的文本文件

    hello.s

    编译后的汇编文件

    hello.o

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

    hello

    链接后的可执行文件

    hello.elf

    用readelf读取hello.o的ELF格式信息

    hello.asm

    反汇编hello.o的反汇编文件

    hello2.elf

    由hello可执行文件生成的.elf文件

    hello2.asm

    反汇编hello可执行文件得到的反汇编文件

参考文献

[1] Randal E.Bryant,David R.O’Hallaron. 深入理解计算机系统