哈工大计算机系统大作业:程序人生

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业        信息安全               

学     号        2021113317               

班     级        2103201               

学       生        孟辰             

指 导 教 师        刘宏伟              

计算机科学与技术学院

2022年5月

摘  要

本文描述了一个简单的hello.c程序在Linux系统中的完整生命周期。从程序的编写到最终在Shell中运行结束,我们通过深入的分析和介绍,了解了它在经过预处理、编译、汇编和链接等环节后生成可执行文件的过程。

在这个过程中,我们涉及到了CPU、RAM、Cache和操作系统等关键组件的相互配合,以及预处理、编译、汇编、链接等重重分析构造。

程序变为进程的这个过程并不容易,我们必须克服各种困难和挑战。不仅涉及Linux系统下的存储管理、进程管理和IO管理等相关知识,而且与虚拟内存和异常信号等概念密切相关。这些知识让我们能够更好地理解程序的执行过程,并学会使用相关工具和技术进行分析和管理。

通过阅读本文,不仅能让我们对程序的生成过程和与之相关的系统管理和技术知识有更全面的认识,而且能够清晰地观察到hello.c程序的完整生命周期,从它被键盘输入,直到最后成为一个运行中的进程。本文将为我们呈现一个富有生命力的程序家族,展示程序在Linux系统中的精彩旅程!

关键词:计算机系统,Linux,程序人生                          

目  录

第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(From Program to Process):

当用户手动输入了一段.c文件源代码(program)后这个过程就开始了。首先,通过预处理器cpp对源代码进行预处理,生成hello.i文本文件。然后,编译器ccl将hello.i编译成汇编代码hello.s。接着,汇编器as将汇编代码翻译成重定位目标文件hello.o。最后,链接器ld将多个可重定位目标文件组合,生成可执行目标文件hello。通过系统的进程创建机制,将可执行文件hello加载到新创建的进程中(progress),实现从程序到进程的转化。

020(Zero-0 to Zero-0):

当进程处于初始状态时,可以将其视为 "从零开始",表示进程还未开始执行任何操作或完成任何任务。这是进程创建的初始状态。而当进程处于终止状态时,可以将其视为 "回到零",表示进程已经完成了它的任务或被终止,不再执行任何操作。这是进程结束的状态。因此,"进程From Zero-0 to Zero-0" 描述了进程从初始状态到终止状态的整个生命周期或过程。这个过程包括进程的创建、执行任务以及最终的终止或结束。

操作系统通过调用fork系统调用创建一个新的子进程,子进程拥有独立的资源,包括虚拟内存空间、寄存器状态和打开的文件描述符等。而后,子进程开始执行时,它会从fork调用的位置开始执行,并继续执行后续的代码,直至当子进程执行完毕或遇到异常情况时,它会终止执行,父进程可以通过调用wait或waitpid等系统调用等待子进程的终止,并获取子进程的退出状态码,当子进程被父进程回收后,它的资源会被释放,包括虚拟内存空间、文件描述符等。这时,它回归于0。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2.11GHz;16GRAM;512G SSD

1.2.2 软件环境

Windows10 64位;Virtual Box 6.1;Ubuntu 20.04 LTS 64位

1.2.3 开发工具

CodeBlocks 64位;vi/vim/gedit+gcc;GDB;OBJDUMP;EDB

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c

源代码

hello.i

预处理后的文本文件

hello.s

编译之后的汇编文件

hello.o

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

hello

链接之后的可执行文件

hello_o.elf

hello.o的ELF格式

hello.elf

hello的ELF格式

hello_o.objdump

hello.o反汇编代码

hello.objdump

hello的反汇编代码

1.4 本章小结

本章概括介绍了hello的P2P和O2O的过程和本实验用到的硬软件环境和开发调试工具、生成的中间结果文件的名字,文件的作用。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理是编译过程的前置步骤,是编译过程中的一个阶段,它是在实际编译源代码之前进行的一系列文本处理操作。通过预处理器对原始的C程序进行修改和处理。它可以实现宏替换、文件包含、条件编译、符号定义等功能,以提高代码的可读性和可维护性。

作用:

 

  1. 宏替换:预处理器能够识别源代码中的宏定义,并在代码中展开宏。宏定义使用#define指令,可以将一段代码或常量定义为宏,然后在代码中使用宏名称进行替换。这样可以提高代码的重用性和可读性,减少代码冗余。
  2. 条件编译:通过条件预处理指令,如#ifdef#ifndef#if等,可以根据条件判断编译不同的代码块。这使得我们可以根据特定的条件(如操作系统类型、编译选项、宏定义等)选择性地编译部分代码,以实现跨平台兼容性或有条件的编译。
  3. 文件包含:预处理器支持通过#include指令将其他源代码文件包含到当前文件中。这使得我们可以将通用的代码片段或库文件包含进来,提高代码的模块化和复用性。文件包含可以嵌套,允许我们构建复杂的源代码文件结构。

2.2在Ubuntu下预处理的命令

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

 

2.3 Hello的预处理结果解析

首先可以直观的看到:文件的大小由548bytes增加到了恐怖的64.7k!!

 

 

我们打开hello.i文件,发现main函数和全局变量的代码没有被改变,而原始的#include语句被替换成了大量头文件中的内容,对源文件中的宏进行了展开操作,包括外部函数的声明、数据结构的定义和数据类型的定义等。同时,在预处理过程中删掉了源程序当中的注释内容。但可以发现,与处理后的hello.i文件实质上还是一个c语言文件。

 

2.4 本章小结

本章首先介绍了预处理的概念与作用,接着演示了hello.c文件如何进行预处理变为hello.i文件,并对与处理后的文件结果进行了分析。

第3章 编译

3.1 编译的概念与作用

概念:编译是指将高级程序语言编写的源代码转换成机器能够执行的目标代码的过程。

作用:将源代码翻译成计算机能够理解和执行的汇编语言指令。通过编译,源代码中的高级语言语句会被转化为汇编语言指令,以便计算机能够按照指令执行程序的逻辑,即源程序翻译成目标程序,把高级语言被翻译成汇编语言。

3.2 在Ubuntu下编译的命令

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

 

3.3 Hello的编译结果解析

1. 定义字符串,包含特定的Unicode字符。

 

2.开辟栈,因为栈倒着生长,所以做减法操作,开辟空间大小为32bytes,main函数变量argc保存在-20(%rbp),argv*保存在-32(%rbp)中

 

3.比较-20(%rbp)(argc)是否等于4,如果等于的话则输出字符串,并exit。如果不等于4的话,跳转到.L2.

 

 

4..L2内容:把0存到-4(%rbp)(局部变量i),并跳转到L3

 

5.将-4(%rbp)(局部变量i)的内容与8作比较,若大于8,则调用getchar,并把0放到寄存器rax准备返回0(return 0)。若-4(%rbp)小于等于8,则跳转.L4

 

6.L4的首先把-32(%rbp)(argv*)放到寄存器rax,用rax+16,和rax+8得到argv[1],argv[2]的地址,再取用改地址取数(movq (%rax),%rdx),分别放到寄存器rdx和寄存器rax中,并调用pintf函数。

 

然后取-32(%rbp)+24的数为地址做取数操作(argv[3]),存到寄存器rax、rdi中,然后使用atoi函数。这里有一点非常值得关注!传入参数为字符串型,所以用寄存器rdi,返回参数为int型,所以用的是寄存器eax!这里完成了一次隐形类型转换!

后面把eax的值放到寄存器edi,使用sleep函数,并给-4(%rbp)中的数(局部变量i)加一。

 

a.文件说明:.file 声明源文件,.text指示代码段,.section指示rodata段。

 

b.数据:定义字符串,包含特定的Unicode字符。

 

c.赋值:使用数据传送指令mov,后面接b,w,l,q分别代表传送数据为8位,16位,32位,64位

 

d.算术操作:加用add,减用sub,均为第二个数op第一个数,存在第二个数的位置上,后面接b,w,l,q分别代表传送数据为8位,16位,32位,64位

 

 

 

e.数组/指针/结构操作:

数组:数组头部指针加偏移量作为地址,做取数操作

指针:指针作为地址,做一次取数操作

结构体:通过结构体内部的偏移量来访问。

 

f.控制转移:

 

(1)If语句:该程序中使用cmp类比较指令将argc与4比较,如果不等于4,则跳转到L2执行。

 

(2)For循环:该程序先使用MOV类数据转移指令将局部变量i赋值为0,没进行完依次循环后i+1,之后使用cmp类比较指令将i与8比较,小于等于8的话跳转到L4。

 

g.函数操作:

puts: 将一个以空字符结尾的字符串发送到标准输出流(通常是屏幕)。该函数会在字符串的末尾自动添加一个换行符,使得下一次输出从新的一行开始

 

exit: 立即终止调用进程

 

printf:向屏幕输出

 

atoi:把字符串转化为int型

 

sleep:实现程序的休眠。

 

getchar:从缓冲区中读取字符。

 

3.4 本章小结

1.汇编语言中的指令是对底层硬件操作的抽象表示,每条指令都有特定的操作码(opcode)和操作数(operand)。

2.汇编语言中的内存访问通过使用内存地址和偏移量来实现。可以使用寄存器或立即数作为地址和偏移量,以读取或写入内存中的数据。

3.汇编语言中的操作指令用于执行各种算术、逻辑和位操作。这些指令可以对寄存器和内存中的数据进行加减乘除、逻辑与或非、移位等操作。

4.在编写x86汇编程序时,需要了解所用的汇编器的语法和约定,并且要注意内存对齐、寄存器保存、函数调用等规范。

通过学习x86汇编语言,我们可以更深入地理解计算机底层的工作原理,并且能够编写高效和优化的程序。掌握汇编语言对于进行系统级编程、调试和性能优化等方面都非常有帮助。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将汇编程序翻译为机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件中。

作用:将汇编语言翻译成计算机能够理解和执行的机器语言指令。

4.2 在Ubuntu下汇编的命令

指令:gcc -c -o hello.o hello.s

 

4.3 可重定位目标elf格式

首先使用指令:readelf -a hello.o > hello_o.elf,得到hello_o.elf文件

 

  1. ELF Header(ELF文件头):
    1. Magic(魔数):用于标识ELF文件格式的特定字节序列。
    2. Class(类别):指定了ELF文件的位数,这里是ELF64,表示64位的ELF文件。
    3. Data(数据编码):指定了数据的编码方式,这里是2's complement和little endian,表示使用补码表示的小端字节序。
    4. Version(版本):指定了ELF文件的版本,当前为版本1。
    5. OS/ABI(操作系统/ABI):指定了目标操作系统和应用程序二进制接口,这里是UNIX - System V。
    6. ABI Version(ABI版本):指定了应用程序二进制接口的版本,这里为0。
    7. Type(文件类型):指定了ELF文件的类型,这里是REL(可重定位文件)。
    8. Machine(目标机器):指定了目标处理器的体系结构,这里是Advanced Micro Devices X86-64(AMD的64位x86处理器)。
    9. Entry point address(入口点地址):指定了程序的入口点地址,这里是0。
    10. Start of program headers(程序头开始位置):指定了程序头表在文件中的偏移量,这里是0。
    11. Start of section headers(节头开始位置):指定了节头表在文件中的偏移量,这里是1240字节。
    12. Flags(标志):指定了与ELF文件相关的标志,这里是0。
    13. Size of this header(ELF头的大小):指定了ELF头的大小,这里是64字节。
    14. Size of program headers(程序头表的大小):指定了程序头表的大小,这里是0字节。
    15. Number of program headers(程序头表的数量):指定了程序头表的数量,这里是0。
    16. Size of section headers(节头表的大小):指定了节头表的大小,这里是64字节。
    17. Number of section headers(节头表的数量):指定了节头表的数量,这里是14。
    18. Section header string table index(节头字符串表索引):指定了节头字符串表在节头表中的索引,这里是13。

  

 

  1. Section Headers(节头): 对各个节头的信息进行了列举,包括节的名称、类型、地址、偏移量、大小、标志。

1.text节,大小为0x92,类型为PROGBITS,标志为AX(只读可执行),偏移量为0X40,

2.rela.text节,大小为0xc0,类型为RELA,标志为I,偏移0x380字节

3.data节,已初始化的全局变量节,类型为PROGBITS,标志为WA(可读写),偏移0xd2

4.bss节,未初始化的全局和静态变量或初始化为0的全局或静态变量,大小为0,类型为PROGBITS,标志为WA,偏移0xd2

5.rodata节,只读数据,大小为0x33,标志为A(只读),偏移0xd8

6.comment节,版本控制信息,大小为0x24,标志为MS,偏移为0xef

7.note.GNU_stack节:标记可执行堆栈,大小为0x0字节,类型为PROGBITS,偏移量为0x12f

8.eh_frame节:处理异常,大小为0x38字节,类型为PROGBITS,偏移量为0x150,标志为A(表明该节的数据只读)。

9.rela.eh_frame节:.eh_frame节的重定位信息,大小为0x18字节,类型为RELA,偏移量为0x440,标志为I。

10.shstrtab节:包含节区名称,大小为0x61字节,类型为STRTAB,偏移量为0x188

11.symtab节:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。大小为0x1b0字节,类型为SYMTAB,偏移量为0x338

12.strtab节:一个字符串表,包括.symtab和.debug节中的符号表,以及节头,偏移量为0x458字节

13shstrtab节:大小为0x74字节,也是一个字符串表,偏移量为0x458字节

 

  1. Relocation section '.rela.text'(重定位节): 列举了重定位节中的条目,包括偏移量、信息、类型、符号值和符号名称。本程序中的重定位函数是:.L0,puts,exit,.L1,printf,atoi,sleep,getchar。

 

  1. Relocation section '.rela.eh_frame'(重定位节): 列举了重定位节中的条目,包括偏移量、信息、类型、符号值和符号名称。

 

 

 

  1. Symbol table '.symtab'(符号表): 列举了符号表中的条目,包括符号的值、大小、类型、绑定、可见性、索引和名称。

4.4 Hello.o的结果解析

使用指令:objdump -d -r hello.o > hello_o.objdump

 

 

我们打开hello_o.objdump文件,并进行观察

 

 

经过观察后,我们发现该文件与hello.s文件大体相同,只有立即数的访问、分支转移、函数调用三点有所差异。

1.立即数的访问:

hello_o.objdump文件中的立即数存储格式均为0x开头,即十六进制

 

 

hello.s文件则是数字,说明是十进制。

 

 

2.分支转移:

hello.s文件是.L3这样的助记符,用户可以阅读,但电脑却无法执行

 

 

hello_o.objdump文件中则采用了相对寻址,通过偏移地址的方式来进行分支转移,电脑可阅读。

 

 

3.函数调用:

在hello.s文件中,只需要call + 函数名@PLT就可表示调用函数

 

 

但在hello_o.objdump文件中则需写出它们的地址(此处地址都为0是因为还没有进行重定位)

 

 

4.5 本章小结

本章我们介绍了汇编的过程,学习了ELF文件的内容以及格式,并分析了重定位前汇编程序和重定位后反汇编的差别,尽管他们看似大体相同,但已是两种截然不同的文件形式了。

5章 链接

5.1 链接的概念与作用

概念:

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。

作用:

  1. 符号解析:链接器负责解析程序中使用的符号,即变量、函数或对象的名称。当程序中引用一个符号时,链接器会找到该符号的定义并建立符号与其定义之间的关联。
  2. 符号重定位:在链接过程中,目标文件或模块中的代码和数据可能会使用相对地址或符号引用来表示内存位置。链接器负责将这些相对地址转换为绝对地址,以确保程序在运行时可以正确访问内存中的代码和数据。
  3. 内存管理:链接器还负责将不同的目标文件或模块中的代码和数据组织在内存中的适当位置。它将代码段、数据段和堆栈等部分组合在一起,并分配适当的内存空间,以便程序可以正确地加载和执行。
  4. 库链接:链接器可以将外部库(如动态链接库或静态链接库)与程序进行链接。这样,程序可以使用库中提供的功能,而无需重新实现这些功能。
  5. 符号重复消除:如果程序中使用了多个目标文件或模块,并且它们都定义了相同的符号(如全局变量或函数),链接器将负责消除重复定义,以避免符号冲突和重复代码。
  6. 优化和代码生成:链接器可以进行一些优化操作,如代码压缩、死代码消除和代码重排等,以提高程序的性能和效率。

5.2 在Ubuntu下链接的命令

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

 

 

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

使用指令:readelf -a hello > hello.elf

1. ELF: 上次的节头数量为13个,这次变为27,其它保持一致。

 

 

2.节头:对各个节头的信息进行了列举,包括节的名称、类型、地址、偏移量、大小、标志。

 

 

3程序头: 程序头部表描述了可执行文件的连续的片映射到连续的内存段的映射关系,描述了可执行文件在内存中的布局和加载信息。每个程序头部对应一个段,包含段的类型、在文件中的偏移、在内存中的虚拟地址、物理地址、大小和访问权限。

 

 

4. 动态段信息,包含了一些动态链接器需要的重要信息,如依赖的共享库、符号表、重定位信息

4.1共享库

 

 

4.2 重定位信息:在这里我们可以注意到.rela.text节消失,说明链接过程完成了重定位操作,但确切地址仍不确定,需动态连接后确定。

 

 

4.3符号表,内容有:

  1. 符号值(Symbol Value):符号的值或地址,表示符号在内存中的位置。
  2. 符号大小(Size):符号所占据的空间大小。
  3. 符号类型(Type):符号的类型,如函数、对象、未定义符号等。
  4. 符号绑定(Bind):符号的绑定类型,指示符号的可见性和链接性,如局部符号、全局符号、弱符号等。
  5. 符号可见性(Vis):表示符号可见性,是否可见。
  6. 符号的节的索引(Ndx):指示了符号所属的段或节的索引。                                                                                                                                                                                                                                                                                                                                                           
  7. 符号名称(Name):符号的名称,如函数名、变量名等。

 

 

5.4 hello的虚拟地址空间

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

 

 

使用edb加载hello,可以看到进程的虚拟地址空间各段信息。可以看出,段的虚拟空间从0x400000开始,到0x400ff0结束。

 

 

.init段起始地址为0x401000,.text段的起始地址为0x4010f0,.rodata段的起始地址为0x402000,.data起始地址为0x404048,也都可以在edb中找到:

 

.text:

 

     .rodata:

     .data:

5.5 链接的重定位过程分析

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

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

使用指令:objdump -d -r hello > hello.objdump

 

 

 

通过观察,我们发现,区别主要有以下几点:

1. hello.o中的汇编代码是从0开始的,而hello中的汇编代码已经使用虚拟内存地址来标记了,从0x400000开始。

2.hello.o中的相对偏移地址,在hello的汇编代码中已经全部计算出了其绝对地址。

 

 

 

链接过程:通过hello.o中跳转指令和call指令后为绝对地址,计算出在hello中重定位后的虚拟地址。具体方法如下:

我们以计算puts的地址为例子:

 

 

首先我们知道,此时PC指向0x40114a,查询发现puts指令地址为0x401090

 

 

 

二者作差,得到ff ff ff 46,小段法写入,接上本来就有的地址e8就得到了该地址:e8 46 ff ff ff。

5.6 hello的执行流程

从加载hello到_start,到call main,以及程序终止的所有过程,其调用与跳转的各个子程序名和程序地址如下所示:

0000000000401000 <_init>

0000000000401090 <puts@plt>

00000000004010a0 <printf@plt>

00000000004010b0 <getchar@plt>

00000000004010c0 <atoi@plt>

00000000004010d0 <exit@plt>  

00000000004010e0 <sleep@plt>

00000000004010f0 <_start>

0000000000401125 <main>

00000000004011c0 <__libc_csu_init>

0000000000401230 <__libc_csu_fini>

0000000000401238 <_fini>

5.7 Hello的动态链接分析

动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射,它使用偏移量表(GOT)和过程链接表(PLT)来实现函数的动态链接。所以在dl_init前后,.got文件会发生改变。查表知:.got文件地址为0x403ff0

 

 

dl_init前:

 

 

dl_init后:

 

 

5.8 本章小结

本章我们学习了链接,链接是将多个目标文件组合成一个可执行文件或共享库的过程。静态链接将所有依赖项都包含在可执行文件中,而动态链接采用延迟加载策略,实现函数的动态链接。动态链接可以实现共享库的重用和节省内存,提高程序的可维护性和运行效率。

并且详细分析了hello.o是怎么链接成为一个可执行目标文件的过程,理解里面每个参数的变化过程。

6章 hello进程管理

6.1 进程的概念与作用

概念:

进程是计算机中正在运行的程序的实例。它是操作系统分配资源和执行任务的基本单位。每个进程都有自己的内存空间、执行状态和所需的系统资源,它是是os对spu执行的程序的运行过程的一种抽象。

作用:进程是程序在计算机中执行的实体。操作系统通过创建进程来运行程序,并将程序的指令和数据加载到进程的内存空间中。进程执行指令,处理数据,实现程序的功能,并且每个进程都运行在独立的内存空间中,互相隔离。这样可以确保进程之间的数据和状态不会相互干扰,好像程序独占地使用内存系统。

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

作用:

  1. 提供命令行界面:bash允许用户通过命令行输入指令来与操作系统进行交互,执行各种操作,如运行程序、管理文件、配置系统等。
  2. 执行脚本:bash可以解释执行脚本文件,这些脚本文件包含一系列的命令和逻辑,可以自动化执行复杂的任务。
  3. 环境配置:bash提供了环境变量的管理功能,可以设置和修改系统的环境变量,影响用户会话和程序的运行环境。

处理流程:

  1. 当用户打开一个终端或启动一个新的shell会话时,bash会显示一个提示符,等待用户输入命令。
  2. bash会读取用户在命令行中输入的内容,包括命令和参数。
  3. bash对用户输入的命令进行解析,识别命令的名称和参数。
  4. 一旦命令被解析,bash会启动相应的程序或执行相应的操作。它会调用内核提供的系统调用来执行命令,操作文件、进程等。
  5. 命令执行完成后,bash将命令的输出结果显示在终端上,供用户查看。
  6. 一条命令执行完成后,bash会再次显示提示符,等待用户输入下一条命令。这个过程将一直循环进行,直到用户主动退出或关闭shell会话。

6.3 Hello的fork进程创建过程

当程序中的代码执行到 "fork" 调用时,操作系统会创建当前进程的一个副本。这个副本称为子进程,而原始的进程称为父进程。系统会为子进程分配一个唯一的进程标识符(PID),以便在操作系统中进行标识和管理。子进程获得了父进程的所有资源的副本,包括代码、数据、堆栈、打开的文件和其他系统资源。这意味着子进程可以在独立的执行环境中运行,并且可以继续执行父进程之后的代码。子进程的初始状态与父进程几乎完全相同,包括程序计数器、寄存器、进程优先级等。但是子进程会继承父进程的一些属性,如用户 ID、文件权限等。子进程和父进程的执行是并行的,它们可以同时执行不同的代码路径。子进程从 "fork" 调用的位置开始执行,而父进程则继续执行其后的代码。在父进程和子进程中,可以通过 "fork" 的返回值来区分它们。在父进程中,"fork" 返回子进程的 PID(大于 0),而在子进程中,"fork" 返回 0。这样,可以根据返回值来执行不同的逻辑。

6.4 Hello的execve过程

调用 "execve" 后,操作系统会加载指定的可执行文件,并将其作为当前进程的执行内容。该调用需要提供目标程序的路径以及一组参数和环境变量。执行过程中,操作系统会将新程序的代码和数据加载到内存中,并更新当前进程的上下文,包括栈、寄存器、程序计数器等。一旦替换完成,原来的程序代码将被完全覆盖,而新程序从其入口点开始执行。

6.5 Hello的进程执行

进程上下文信息:

进程上下文信息是操作系统中用于管理和描述进程状态的关键数据集合。它包含了进程执行所需的各种关键信息,以确保在进行进程切换时能够保存和恢复进程的执行状态。这些信息包括寄存器状态,如通用寄存器、程序计数器和栈指针,用于记录当前执行位置和执行状态;内存管理信息,如内存布局、地址空间映射和页表,用于管理进程的内存资源;文件描述符,用于记录进程打开的文件和网络连接信息;运行时堆栈,保存函数调用和返回过程中的局部变量和调用上下文信息等。

通过维护和切换进程上下文信息,操作系统能够在不同进程之间实现高效的调度和执行,从而实现多任务处理和资源共享。上下文切换包括保存当前进程或线程的执行状态和环境信息,并加载下一个要执行的进程或线程的执行状态和环境信息,使得多个进程或线程可以交替执行,共享处理器资源,并实现并发执行的效果。

进程时间片:

进程时间片是操作系统中用于调度进程执行的固定时间段。每个进程被分配一个时间片,在该时间片内执行其指令和任务。当时间片用完后,操作系统会触发上下文切换,将处理器控制权转移到下一个就绪的进程上。

调度过程:

在我们的Hello程序中,当执行到sleep系统调用时,会触发陷阱异常。这将导致程序从用户模式切换到内核模式,程序进入休眠状态,操作系统将控制权转移到其他进程。sleep时间结束后,内核会收到一个信号,然后进入内核状态来处理异常,Hello程序得以重新回到用户模式。

接着,当执行getchar函数时,该函数使用read系统调用来读取输入。这再次触发上下文切换,将程序从用户模式切换到内核模式,以执行read系统调用。读取完成后,控制权再次交回给Hello程序,程序继续执行后续的操作。通过这些上下文切换,程序在用户模式和内核模式之间进行交替,以完成不同的系统调用和处理异常的操作。

6.6 hello的异常与信号处理

1、异常和信号异常

异常和信号异常分为四类:中断、陷阱、故障、终止

 

 

2hello执行中可能出现的异常:

  1. 中断(Interrupt):中断是异步发生的,由处理器外部的I/O设备发送的信号引起。中断可以被用来处理各种事件,如时钟中断、输入/输出中断等。当发生中断时,操作系统会暂停当前的程序执行,并跳转到相应的中断处理程序。不同的中断对应不同的信号,例如时钟中断对应SIGALRM信号,键盘中断对应SIGINT信号。
  2. 陷阱(Trap):陷阱是有意的异常,是执行某条指令的结果。在Hello程序中,调用sleep函数会触发陷阱,操作系统会捕获该陷阱,并执行相应的处理操作。
  3. 故障(Fault):故障是由错误引起的异常,通常是一些可修正的错误。在Hello程序执行过程中,可能会出现缺页故障(Page Fault),系统发出SIGSEGV信号。操作系统会捕获该故障,并进行页面调度和加载,以满足程序的内存访问需求。
  4. 终止(Abort):终止是由不可恢复的致命错误引起的结果,通常是一些硬件错误。当发生终止时,操作系统会终止程序的执行,并向其发送相应的信号,如SIGSEGV信号(段错误)或SIGBUS信号(总线错误)。

3、键盘上操作导致的异常:

(1)运行时乱按(包括回车):可以发现该方式并不会影响程序的正常运行,因为它们都会被最后的getchar函数读取,并覆盖掉。

 

 

(2)按Ctrl-Z按键:我们发现该程序被挂起了,因为这样会发送SIGTSTP信号给前台进程组的每个进程,结果是停止前台作业

 

 

(2.1)可以通过jobs指令来查看当前的作业,可以看出当前的作业是hello进程,且状态是已停止

 

 

(2.2)使用ps命令可以查看当前所有进程以及它们的PID,进程包括bash,hello以及ps。

 

 

(2.3)使用pstree命令将所有进程以树状图形式显示

 

 

 

 

 

 

(2.4)使用fg命令可以使停止的hello进程继续在前台运行。

 

 

(2.5)使用kill -9 -进程号可以杀死进程

 

 

(3)按Ctrl-C按键:可以看到进程直接被停止了,这会发送SIGINT信号给前台进程组的每个进程,结果是终止前台进程。

 

 

使用ps指令查看进程,发现该进程确实已经被杀死并回收。

 

 

6.7本章小结

在本章中,我们学习了关于hello进程管理的相关概念和机制。我们了解了进程的概念,它是程序的执行实例,由操作系统进行管理以实现程序的正确执行;进程的创建过程,包括fork创建子进程和execve加载新程序,学习了进程的上下文切换,它是在多任务环境下实现进程调度和切换的重要机制。最后通过学习hello进程管理,我们对进程的创建、执行、上下文切换以及与异常和信号相关的处理有了更深入的了解。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址:逻辑地址是指程序在逻辑上使用的地址,通常是指程序中指令或数据的地址。在Hello程序中,逻辑地址可以是指向变量的指针或函数的入口地址。

2.线性地址:线性地址是指逻辑地址经过地址转换后得到的地址。在操作系统的虚拟内存管理中,线性地址是一个中间的地址空间,它通过分页或分段机制将逻辑地址转换为线性地址。线性地址通常是连续的地址空间。

3.虚拟地址:虚拟地址是指线性地址经过虚拟内存管理机制转换后得到的地址。虚拟地址空间是给进程提供的独立的地址空间,每个进程都有自己的虚拟地址空间。在Hello程序中,进程使用的地址都是虚拟地址。

4.物理地址:物理地址是指最终在内存中的实际地址,它是CPU发送给内存控制器的地址。在虚拟内存管理中,虚拟地址需要经过地址映射,将虚拟地址转换为物理地址,以便访问实际的内存数据。

在Hello程序的执行过程中,逻辑地址和虚拟地址是程序中使用的地址。操作系统通过地址转换机制将虚拟地址转换为线性地址,再通过页表或段表将线性地址转换为物理地址,最终将数据加载到实际的内存位置进行访问。这样,程序可以使用逻辑地址和虚拟地址进行编程,而无需考虑实际的物理内存地址。

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

程序通过一个称为段选择器的值来选择要使用的段。段选择器包含段的索引和特权级等信息,通过访问全局描述符表或局部描述符表获取对应的段描述符。根据选择器选择到的段描述符包含了段的基地址和限长信息,通过解析段描述符,可以获取到段的基地址和限长。再根据逻辑地址中的偏移量,与段的基地址相加,得到线性地址。线性地址是一个中间地址,仍然需要进一步转换为物理地址。线性地址可能会再经过页表机制进行变换,将线性地址转换为物理地址,以便访问实际的内存数据。

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

在Hello程序中,线性地址首先被划分为页号和页内偏移量两部分。页号用于确定所在的页表项,而页内偏移量表示相对于页起始地址的偏移量。系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。通过页目录基址寄存器(PDBR)获取页目录表的起始地址。根据线性地址的高位部分(高10位),在页目录表中找到对应的页目录项(PDE),每个页目录项记录了一个页表的基址。使用页表基址和线性地址的中间部分(中间10位),在页表中找到对应的页表项(PTE),每个页表项包含了一个页框的物理基址。取得页表项中的页框物理基址,将其与线性地址的低位部分(低12位)进行组合,得到最终的物理地址。

 

 

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

当程序访问一个虚拟地址时,用MMU进行地址翻译时,会先将VPN传给TLB,看TLB中是否已经缓存了需要的PTE。CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配。如果存在,即TLB命中,系统可以直接从TLB中获取对应的物理地址,则得到 PPN(40bit)与 VPO(12bit)组合成 PA(52bit),这样省去了访问页表的开销,加快了地址变换速度。

如果TLB未命中,系统将按照四级页表的层次结构逐级访问页表,进行逐级的地址转换,直到找到对应的物理页框。首先,从线性地址中提取出四级页表的索引,根据索引在第一级页表(一级页目录)中找到对应的页表项,根据第一级页表项中记录的物理页表的基址,加上线性地址的中间部分,得到第二级页表的物理地址。未命中则依此查找第二级、第三级页表。

在这个过程中,每一级的页表都提供了下一级页表的物理地址。通过层层递进的方式,系统最终可以从四级页表中获取到对应虚拟地址的物理地址。

 

 

 

 

 

 

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


在具备三级缓存(Cache)支持的情况下,物理内存访问可以通过缓存来提高访问速度和效率。三级缓存由L1、L2和L3三个层级组成,每个层级的缓存容量逐级增大,但访问延迟也逐级增加。

当程序进行物理内存访问时,系统首先会检查L1缓存,也称为指令缓存(Instruction Cache)和数据缓存(Data Cache),来确定是否存在所需数据的副本。对Cache的访问需要把一个物理地址分为标记(CT)、组索引(CI)、块偏移(CO)三个部分。首先我们通过组索引来找到我们的地址在Cache中所对应的组号,再通过标记和Cache的有效位来判断我们的内容是否在Cache中。如果数据在L1缓存中命中,即缓存命中,系统可以直接从L1缓存中获取所需数据,从而避免了访问主内存的开销,提高了访问速度。

如果数据在L1缓存未命中,系统会继续检查L2缓存,也称为高速缓存(Unified Cache)。L2缓存容量比L1缓存大,但访问延迟也相应增加。如果数据在L2缓存中命中,系统将从L2缓存中获取数据,避免了直接访问主内存,尽管相比于L1缓存,访问速度略有下降。当数据在L2缓存未命中时,系统将进一步检查L3缓存。

如果数据在三级缓存中都未命中,系统将通过主内存来获取所需数据。此时会发生缓存不命中(Cache Miss),系统将从主内存中读取数据,并将其加载到适当的缓存层级中,以供后续访问使用。

值得注意的是:缓存的容量有限,如果数据无法在缓存中找到,就会发生缓存不命中,需要从主内存中获取数据,这会导致较高的访问延迟。因此,在设计程序时,需要合理利用缓存,并尽可能提高缓存的命中率,以获得更好的性能。

7.6 hello进程fork时的内存映射

当父进程调用fork函数时,操作系统会创建一个新的进程,即子进程。子进程将复制父进程的地址空间,包括代码段、数据段、堆段和栈段,并分配一个与父进程不同的唯一的PID。这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射

通过execve函数,Hello进程能够加载一个新的可执行文件,替换当前进程的执行内容。并且,execve函数加载新程序时会替换当前进程的内存映射,原来的代码和数据会被新程序的内容取代。

加载并运行hello需要以下结构步骤:

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

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

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

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

 

 

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

当发生缺页故障时,处理器会触发缺页中断,并将控制权交给操作系统内核。此时,内核可以选择发送一个信号SIGSEGV给当前进程,以通知它发生了缺页故障。操作系统会根据中断产生的信息,确定缺失页面的虚拟地址,并通过MMU在页表中进行查找。如果页面在页表中不存在或者无效,说明程序访问的页面尚未调入内存。操作系统会根据缺失页面的信息,从辅存中将页面加载到物理内存中的一个页。一旦页面调入到内存中,操作系统会更新页表,将对应的虚拟页面映射到物理页面。完成缺页处理后,操作系统将恢复程序的状态,并重新执行导致缺页故障的指令。这使得程序可以访问所需的页面。

7.9动态存储分配管理(未学习该部分内容以及第八章内容)

7.10本章小结

我们深入了解了Hello程序中的存储管理过程。在fork进程时,通过页表和写时复制技术实现了父子进程之间的内存映射,避免了完全复制内存的开销。而在execve时,通过重新映射程序的逻辑地址空间,将新的程序加载到内存中,并更新页表,实现了内存的重新映射。并且详细学习了虚拟地址与物理地址之间映射变换的全过程以及缺页故障和缺页中断处理的过程。

结论

hello所经历的过程

  1. 源程序:在文本编辑器或集成开发环境中编写C语言代码,得到最初的hello.c源程序。
  2. 预处理:预处理器对源程序进行处理,包括宏展开、文件包含和条件编译等操作,生成ASCII码的中间文件hello.i。
  3. 编译:编译器将预处理后的代码翻译成汇编指令,生成ASCII汇编语言文件hello.s。
  4. 汇编:汇编器将汇编指令翻译成机器语言,并生成包含重定位信息的可重定位目标文件hello.o。
  5. 链接:链接器对可重定位目标文件进行符号解析、重定位和动态链接等操作,生成可执行目标文件hello。此时,hello程序可以被执行。
  6. fork创建进程:在shell中运行hello程序时,shell调用fork函数创建一个子进程,该子进程将成为hello程序的副本。
  7. execve加载程序:子进程中调用execve函数,加载hello程序的可执行目标文件,将进程的上下文切换到hello程序的入口点,hello程序开始运行。
  8. 运行阶段:操作系统内核负责调度进程的执行,并对可能发生的异常和信号进行处理。内存管理单元(MMU)、转换后备缓冲(TLB)、多级页表、缓存和动态内存分配器等组件相互协作,共同管理内存资源。同时,通过Unix I/O机制,hello程序与文件进行交互。
  9. 终止:hello进程运行结束,操作系统的内核负责回收终止的hello进程,清理其所占用的资源和数据结构。

感悟:

输入一串简单、朴实无华的“hello”,这样简单的一个程序,从被输入到在电脑屏幕上输出出来,在没有进行计算机系统这门课程的学习之前,我是无法想象这背后蕴藏着的巨大的工作量与任务量的,在学习的过程中,我一次又一次被前人(计算机工作、设计者)的智慧所惊讶!一个又一个精妙的设计,使得计算机有条不紊的运行,使得人类社会在计算机的帮助下飞速前进!

hello的程序人生虽只有短短的零点几秒,让我不禁联想到:每个人之于千百万年的人类历史长河,虽寄蜉蝣于天地,渺沧海之一粟,但正是一个又一个人的出现推动了人类文明的进步,一个又一个短暂的hello“人生”则构造了宏大壮丽的计算机世界!

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

附件

hello.c

源代码

hello.i

预处理后的文本文件

hello.s

编译之后的汇编文件

hello.o

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

hello

链接之后的可执行文件

hello_o.elf

hello.o的ELF格式

hello.elf

hello的ELF格式

hello_o.objdump

hello.o反汇编代码

hello.objdump

hello的反汇编代码

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

参考文献

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

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

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值