ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

程序人生-Hello‘s P2P

2021-06-27 17:57:29  阅读:325  来源: 互联网

标签:预处理 文件 程序 Hello 地址 人生 P2P 进程 hello


 

  

 

计算机系统​​

 

大作业

 

 

题     目  程序人生-Hellos P2P  

专       业     计算机科学与技术   

学     号     1190201324         

班    级     1903006            

学       生     刁奕宁          

指 导 教 师     史先俊          

 

 

 

 

 

 

计算机科学与技术学院

2021年5月

  摘  要

本文介绍了一个hello.c程序的医生,分析了程序由诞生执行再到消亡的过程。通过对hello程序P2P和020的整体介绍,在linux操作系统下对C语言程序hello.c的运行全过程进行了分析。 分析了从c文件转化为可执行文件过程中的预处理、编译、汇编和链接阶段以及可执行文件执行过程中的进程管理、存储空间管理和I/O管理的原理。

 

关键词:程序人生;预处理;编译;汇编;链接;进程;异常;虚拟内存;I/O函数                        

 

 

 

 

 

 

目  录

 

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.2.1 硬件环境

1.2.2 软件环境

Windows 10 64位 ;Vmware 14 ;Ubuntu 18 64位

1.2.3 开发工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

6章 HELLO进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

本章介绍了程序在shell执行及进程的相关概念,描述了shell是如何在用户和系统内核之间建起一个交互的桥梁。程序在shell中执行是通过fork函数及execve创建新的进程并执行程序。hello程序在进程的作用下已经可以发挥程序的作用,并使得各个进程可以并发执行而不会产生冲突矛盾。

7章 HELLO的存储管理

7.1 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动态存储分配管理

7.10本章小结

8章 HELLO的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

参考文献

 

 

 
 
第1章 概述

   1.1 Hello简介

P2P:

在编译器的处理下,hello.c文件经历预处理、编译、汇编、链接,四个步骤,变为可执行文件,然后由shell为其创建一个新的进程并运行它。

过程如下:

  

图 1.1 CSAPP 图1-3 编译系统

020:

shell通过execve加载并执行hello,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后执行第一条指令,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

   1.2 环境与工具

    1.2.1 硬件环境 

X64 CPU ;1.80GHZ ;8.00G RAM ;256GHD Disk

1.2.2 软件环境

Windows 10 64位 ;Vmware 14 ;Ubuntu 18 64位

1.2.3 开发工具

Visual Studio 2019 64位 ;CodeBlocks 64位

1.3 中间结果

文件名称

文件描述

hello.c

hello的c语言代码(源文件)

hello.i

预处理后的文件

hello.s

编译后产生的汇编文件

hello.o

可重定位的目标文件

hello

可执行文件

 

 1.4 本章小结

本章介绍了hello程序诞生的P2P与O2O过程,以及实验的环境,所需工具和程序的中间文件。hello.c程序从编写、预处理、编译、汇编、链接再到执行,体现了计算机系统系统各部分的具体功能,以及它们之间的的协同合作。

 

 


第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

预处理的作用:

根据源代码中的预处理指令修改你的源代码。预处理指令是一种命令语句(如#define),它指示预处理程序如何修改源代码。在对程序进行通常的编译处理之前,编译程序会自动运行预处理程序,对程序进行编译预处理,这部分工作对程序员来说是不可见的。

预处理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本。在预处理版本中,宏和常量标识符已全部被相应的代码和值替换掉了。如果源代码中包含条件预处理指令(如#if),那么预处理程序将先判断条件,再相应地修改源代码。

在集成开发环境中,编译,链接是同时完成的。其实,C语言编译器在对源代码编译之前,还需要进一步的处理:预编译。预编译的主要作用如下:

1、将源文件中以”include”格式包含的文件复制到编译的源文件中。

2、用实际值替换用“#define”定义的字符串。

3、根据“#if”后面的条件决定需要编译的代码。

预处理名称

意义

#define

宏定义

#undef

撤销已定义过的宏名

#include

使编译程序将另一源文件嵌入到带有#include的源文件中

#if

#if的一般含义是如果#if后面的常量表达式为true,则编译它与#endif之间的代码,否则跳过这些代码。命令endif标识一个#if块的结束。#else命令的功能有点像c语言中的else,#else建立另一选择(在#if失败的情况下)。#elif命令意义与else if相同,它形成一个if else-if阶梯状语句,可进行多种编译选择。

#else

#elif

#endif

#ifdef

用#ifdef与#ifndef命令分别表示“如果有定义”及“如果无定义”,是条件编译的另一种方法。

#ifndef

#line

改变当前行数和文件名称,他们是在编译程序中预先定义的标识符命令的基本形式如下:

#line number[“filename”]

#error

编译程序时,只要遇到#error就会生成一个编译错误提示信息,并停止编译

#pragma

为实现时定义的命令,它允许向编译程序传送各种指令例如,编译程序可能有一种选择,它支持对程序执行的跟踪。可用#pragma语句指定一个跟踪选择。

2.2在Ubuntu下预处理的命令

在控制台输入预处理命令:gcc -E hello.c -o hello.i ,生成hello.i文件 (预处理后的文件)。

 

图 2.2 linux下预处理指令

2.3 Hello的预处理结果解析

经过预处理过程生成的hello.i 文件共计3105行,其中原本的main函数被放置在了最后,而hello.c预编译引用的头文件 (stdio.h  unistd.h  stdlib.h) 被放置在了文档前面。

预处理过程中预处理器 (cpp) 识别到#include这种指令就会在环境中搜索该文件并将其递归展开

 

图 2.3.1 hello.i中main程序

 

标记了一些头文件的位置:

 

#include

 

 

图 2.3.2 hello.i中stdio.h

 

#include

 

 

图 2.3.2 hello.i中unistd.h

 

#include

 

 

图 2.3.3 hello.i中stdlib.h

 

将引用到的头文件完全复制,加入到hello.i文件中

 

 

图 2.3.4 hello.i中引用到的头文件

 

2.4 本章小结

本章节展示分析了预处理的过程,在预处理过程中,hello.c经过一系列的cpp处理生成hello.i文件。分析了hello.i中文本的含义以及和hello.c的关联。

对于hello的成长过程还需要编译器的进一步执行分析。


第3章 编译

3.1 编译的概念与作用

编译的概念:

将高级语言所写的源程序翻译成等价的机器语言或汇编语言的目标程序。

编译的作用:

将高级语言的文件翻译成机器语言或汇编语言文本,使得机器能够更加容易理解和执行。(从.i文件到.s文件,即预处理后的文件到生成汇编语言程序)

编译主要过程:

1.将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号。

2.基于词法分析得到的一系列记号,生成语法树。

3.由语义分析器完成,指示判断是否合法,并不判断对错。

4.中间代码(语言)使得编译器分为前端和后端,前端产生与机器(或环境)无关的中间代码,编译器的后端将中间代码转换为目标机器代码,目的:一个前端对多个后端,适应不同平台。

5.编译器后端主要包括:

代码生成器:依赖于目标机器,依赖目标机器的不同字长,寄存器,数据类型等

目标代码优化器:选择合适的寻址方式,左移右移代替乘除,删除多余指令。

3.2 在Ubuntu下编译的命令

在控制台输入编译命令:gcc -S hello.i -o hello.s ,生成hello.s文件 (编译后的汇编文件)。

 

图 3.2 linux下编译指令

3.3 Hello的编译结果解析

3.3.1 辅助信息

 

图 3.3.1 hello.s中辅助信息

1. .file:声明源文件,记录了源文件的名称属性。

2. .text:声明代码段。

3. .section .rodata: 声明只读代码段。

4. .align:声明对指令或数据的存放地址的对齐格式。

5. .string:声明字符串类型数据,保存了程序运行过程中所需要的字符串。

6. .globl:声明全局变量。

7. .type:指定函数类型或对象类型。

3.3.2 数据

hello.c的程序中使用到int,字符串以及数组三种数据类型

源程序:

 

图 3.3.2 hello.c源程序

3.3.2.1 int

1.局部变量int i

局部变量存储在寄存器或者栈空间中,hello.c程序中i作为局部变量,在程序中声明,存放在栈空间-4(%rbp)之中,i是4字节的

 

图 3.3.2.1.1 hello.s中对局部变量i的声明

2. main函数参数 int argc

argc是main函数的第一个参数,不需要声明,直接调用即可。程序中-20(%rbp)对argc进行引用。

 

图 3.3.2.1.2 hello.s中对argc的引用

3. 立即数

exit(1);中对于1的引用

 

图 3.3.2.1.2 hello.s中对立即数的存储使用

3.3.2.2 字符串

 

 

图 3.3.2.2.1 hello.c源程序中的字符串

两个字符串被保存到了rodata段中。

 

 

图 3.3.2.2.2 hello.s对字符串的引用

3.3.2.3 数组

数组char *argv[]作为main的第二个参数,没有单独声明,在函数执行时在命令行进行输入。

argc指针指向已经分配好连续的连续空间,起始地址为argv (char* 指针类型)。

程序中使用到argv[1],argv[2]以及argv[3]三个char*数据。

 

图 3.3.2.3 hello.s对数组的引用

3.3.3 赋值操作

对局部变量i的赋值:

通过movl语句对四字节的i进行赋初值操作

 

图 3.3.3 hello.s对局部变量i的赋值

3.3.4 类型转换

在将argv[3]的值保存在寄存器%rdi之后,执行atoi函数,把字符串转换成整型数。

 

图 3.3.4 hello.s对atoi函数的调用

3.3.5 算术操作

基本指令

效果

leaq S, D

D = &S

INC D

D = D + 1

DEC D

D = D - 1

NEG D

D = -D

NOT D

D = ~D

ADD S, D

D = D + S

SUB S, D

D = D - S

IMUL S, D

D = D × S

 

特殊指令

效果

描述

imulq S

mulq S

R[%rdx]: R[%rax] = S × R[%rax]

R[%rdx]: R[%rax] = S × R[%rax]

有符号全乘法

无符号全乘法

idivq S

R[%rdx] = R[%rdx]: R[%rax] mod S

R[%rdx] = R[%rdx]: R[%rax] ÷ S

有符号除法

Divq S

R[%rdx] = R[%rdx]: R[%rax] mod S

R[%rdx] = R[%rdx]: R[%rax] ÷ S

无符号除法

 

1. 加法:ADD S, D (D = D + S)

 

图 3.3.5.1 hello.s对加法指令的应用

2. 减法:SUB S, D (D = D - S)

 

图 3.3.5.2 hello.s对减法指令的应用

3.3.6 关系操作

指令

效果

描述

CMP S1, S2

S2 - S1

比较-设置条件码

TEST S1, S2

S1 & S2

测试-设置条件码

SET** D

D = **

按照设置条件将条件码设置D

 

1. 第一次比较:

判断argc不等于4,将条件码设置为argc - 3,为下一句跳转指令做准备。

 

图 3.3.6.1 hello.s对比较指令的第一次应用

2. 第二次比较:

判断i小于8,将条件码设置为i - 8,为下一句跳转指令做准备。

 

图 3.3.6.2 hello.s对比较指令的第二次应用

3.3.7 控制转移

指令

跳转条件

描述

jmp

1

无条件

je

ZF

相等/结果为0

jne

~ZF

不相等/结果不为0

js

SF

结果为负数

jns

~SF

结果为非负数

jg

~(SF^OF) & ~ZF

大于(符号数)

jge

~(SF^OF)

大于等于(符号数)

jl

(SF^OF)

小于(符号数)

jle

(SF^OF) | ZF

小于等于(符号数)

ja

~CF & ~ZF

大于(无符号数)

jb

CF

小于(无符号数)

 

1. if(argc!=4):

cmpl进行了argv与4的比较,并设置条件码,使用je判断ZF标志位,如果为0,则说明argv - 4 == 0,进而说明 argv == 4,进行跳转到.L2,反之顺序执行if内部程序。

 

图 3.3.7.1 hello.s对if语句的跳转判断

 

2. for(i=0;i<8;i++):

i作为进行计数的变量循环8次,在每一次循环开始时,cmpl进行了i与7的比较,并设置条件码,如果i<=7,则跳入.L4中执行for语句内部程序,反之顺序执行for语句外部后续程序。

 

图 3.3.7.2 hello.s对for语句的跳转判断

3.3.8 函数操作

参数传递及函数调用:

当程序调用一个函数时,首先将函数所需要传入的参数保存寄存器之中,之后执行一个call指令进行跳转。

返回值:

函数的返回值一般保存在寄存器%eax之中,当需要设定返回值时,将返回值保存到寄存器%eax中,之后使用ret语句进行跳转返回。

1. printf函数:

第一次:

printf将%rdi设置为第一个字符串的首地址 (.LC0),因为只有一个字符串参数,所以call选用puts函数进行输出。

 

图 3.3.8.1.1.1 hello.s对printf的第一次引用

 

图 3.3.8.1.1.2 字符串.LC0中存储的字符

第二次:

printf将%rdi设置为第二个字符串的首地址 (.LC1),将%rsi设置为argv[1],将%rdx设置为argv[2],call选用printf函数进行输出。

 

图 3.3.8.1.2.1 hello.s对printf的第2次引用

 

图 3.3.8.1.2.2 字符串.LC1中存储的字符

2. exit函数:

将寄存器%edi设置为1,call调用exit函数。

 

图 3.3.8.2 hello.s对exit的引用

3. sleep函数以及atoi函数:

先将%rdi设置为argv[3],call调用atoi函数进行字符串到整型数的转换,将转换后的值保存到%edi中,call调用sleep函数。

 

图 3.3.8.3 hello.s对sleep函数以及atoi函数的引用

3.4 本章小结

本章分析了从预编译文件 (.i) 到汇编文件 (.s) 的过程,并详细分析了.s文件的程序代码,包括C语言中的各种数据类型以及各种操作指令,hello更加接近机器层面,对于hello的成长过程还需要汇编器器的进一步执行分析。


第4章 汇编

4.1 汇编的概念与作用

汇编的概念:

汇编器将汇编语言 (.s) 翻译成机器语言指令,并将这些指令打包成可重定位目标二进制文件 (.o) 之中。汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

汇编的作用:

产生机器语言指令,使得机器更加方便执行程序代码。

汇编的过程:

用助记符(Mnemonic)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。

4.2 在Ubuntu下汇编的命令

在控制台输入汇编命令:as hello.s -o hello.o ,生成hello.o文件 (机器语言二进制程序)。

 

 

图 4.2 linux下汇编指令

4.3 可重定位目标elf格式

1.ELF Header:

包含帮助链接器语法分析和解释目标文件的信息,其中包括16字节的表示信息、文件类型类别、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。

 

图 4.3.1 ELF Header

2.Section Header:

包含了文件中出现的各个节的语义,相关信息包括节的名称、类型、地址、偏移量、对齐等。

从节头部表可以得知hello.o共13个节,初始位置在0x488。

 

图 4.3.2 Section Header

3..rela.text:

重定位节,一个.text节中位置的列表,包含了.text(具体指令)节中需要进行重定位的信息。当链接器将目标文件和其他文件组合或由.o文件生成可执行文件时,需要修改一下函数位置(puts, exit, printf, atoi, sleep, getchar)以及.rodata中LC0和LC1两个字符串中的信息,进行重定位声明。

 

 

图 4.3.3 .rela.text

4.4 Hello.o的结果解析

在控制台输入反汇编命令objdump -d -r hello.o,生成反汇编代码

 

 

图 4.4.1 hello.o生成的反汇编代码

 

图 4.4.2 hello.s中汇编代码

汇编代码与反汇编代码的区别:

.o文件反汇编之后代码区域并没有太大的差别,而左侧增加了汇编语言所对应的机器语言指令。这里的机器语言就是由0/1所构成的二进制文件,在终端显示时转换为16进制显示。

1. 分支转移:

hello.s文件中分支转移到目标位置都是使用.L*来表示的,而在hello.o文件反汇编之后,这些目标位置则被具体的地址所代替。段名称在汇编语言之中只是便于编写的助记符,所以在反汇编生成的机器语言中不存在。

2. 函数调用:

hello.s文件中,函数调用是直接使用call + 函数名进行调用,而在hello.o文件反汇编之后,call的目标地址变成当前的下一条指令。C语言中使用的函数属于外部函数库,在执行时需要进行链接才能知道其运行时的执行地址,故目标设置为下一条指令,等待后续操作静态链接后进一步确定其地址。

4.5 本章小结

本章分析了从汇编文件 (.s) 到机器指令 (.o) 的过程,通过查看ELF的信息分析了解了汇编过程中,程序发生的变化以及汇编指令到机器指令的变换过程。通过对比汇编文件和反汇编的代码,分析了汇编指令和机器指令的不同之处。hello重定位推进了进一步的操作,对于hello的进一步成长还需要进行链接才可以变成可执行文件。


 5章 链接

5.1 链接的概念与作用

链接的概念:

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

链接的作用:

链接的存在可以将大型的程序分解为更小的更具有模块化的程序,并可以独立的修改、编译这些模块,降低了程序模块化编程的难度,使分离编译成为了可能。

5.2 在Ubuntu下链接的命令

在控制台输入链接命令:

ld -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 /usr/lib/gcc/x86_64-linux-gnu/7/crtbegin.o  hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

生成链接后的可执行文件。

 

图 5.2 linux下链接指令

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

1.ELF Header:

描述了ELF表中的各项信息。

 

图 5.3.1 ELF Header

2.Section Header:

描述了各节的信息,从节头部表可以得知hello共28个节,初始位置在0x1980。

 

 

图 5.3.2 Section Header

3..rela.text:

 

图 5.3.3 .rela.text

5.4 hello的虚拟地址空间

用edb打开hello,在Data Dump窗口中分析hello加载到虚拟内存的情况以及各段信息。

可执行文件中加载的信息从0x00400000处开始存放,hello中存放位置从0x00400000到0x004000c0。

 

图 5.4 edb下hello的虚拟地址

5.5 链接的重定位过程分析

在控制台输入反汇编命令objdump -d -r hello,生成反汇编代码 

 

 

 

 

图 5.5.1 hello.o生成的反汇编代码

 

图 5.5.2 hello.o生成的反汇编代码

hello的反汇编代码中不同节的含义:

.init:程序初始化需要执行的代码。

.plt:动态链接表-过程链接表。

.text:主要代码及函数。

.fini:程序终止时需要执行的代码。

hello的反汇编代码和hello.o反汇编代码的区别:

1. hello的反汇编代码从.init节开始,而hello.o的反汇编代码从.text节开始。

2. hello的反汇编代码中导入了puts、printf、atoi、getchar、sleep等在主程序中使用过的函数,而hello.o的反汇编代码中不包含这些函数。

3. hello的反汇编代码中函数的调用方法同hello.s,使用call + 函数名直接调用,而hello.o的反汇编代码使用call指向下一条语句,并未直接调用函数。

5.6 hello的执行流程

gdb下函数单步运行的流程以及函数地址

 

图 5.6 hello的gdb分析

5.7 Hello的动态链接分析

在对hello的readelf分析中得知,.got表的地址为0x0000000000600ff0,通过edb中对Data Dump窗口跳转,定位到GOT表处。

 

图 5.7.1 .got表的相关信息

 

图 5.7.2 edb定位GOT表

 

图 5.7.2 调用_init后的GOT表

在开始edb的调试后,初始的地址0x00600ff0全为0。对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行地址,所以需要添加重定位记录,等待动态链接器的处理,为避免运行时修改调用的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT + 全局变量偏移表GOT实现函数的动态链接,GOT中存放目标函数的地址,PLT使用该地址跳转到目标位置,其中GOT[1]指向重定位表,GOT[2]指向动态链接器ld-linux.so运行地址。

5.8 本章小结

本章分析了程序链接的过程,通过查看ELF的信息分析了解了链接生成可执行文件过程中,程序发生的变化。链接作为程序编写中重要的手段,在程序的修改中扮演着重要的角色。

 


6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

狭义定义:进程是正在运行的程序的实例。

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

进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

进程的作用:

进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。

进程的三种状态

就绪状态:已经分配到除CPU以外的所有资源,加在就绪队列中

执行状态:进程已经获得CPU,正在执行。

阻塞状态:正在执行的进程发生了某事件(I/O,申请缓存失败等)暂时无法继续执行的状态,此时引起进程调度,把处理机分给另一个进程,它进入阻塞队列。

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

shell的作用:

shell是个一个交互型应用及程序,在操作系统中提供了一个用户与系统内核进行交互的界面,代表用户访问操作系统内核服务,基本作用是解释并运行用户的指令。

处理流程:

1. 读取从终端输入的用户命令。

2. 分析命令行字符串,获取命令行参数,构造传递给execvedargv向量。

3. 检查第一个命令行参数是否是一个内置的shell命令。

4. 否则用fork为其分配子进程并运行。

5.子进程中,进行步骤2获得参数,调用exceve()执行制定程序。

6.命令行末尾没有&,代表前台作业,shell使用waitpid等待作业终止后返回。

7.命令行末尾有&,代表后台作业,shell返回。

6.3 Hello的fork进程创建过程

终端程序调用fork函数,首先输入./hello指令,shell读入命令。父进程通过fork函数创建新的运行子进程hello。

子进程的特点:

1. 子进程几乎但不完全与父进程相同,子进程拥有与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码、数据段、堆、共享库以及用户栈。

2. 子进程拥有与父进程任何打开文件描述符相同的副本,意味着子进程可以读写父进程中打开的任何文件。

3. 子进程和父进程最大的差别在于他们有着不同的PID,其中父进程的PID非0,而子进程PID为0。

4. 父进程与子进程使并发独立的进程,内核能够以任意方式交替执行它们的逻辑控制流指令。在子进程执行期间,父进程默认选项使显示等待子进程的完成。

6.4 Hello的execve过程

execve函数在当前进程的上下文加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。只有当出现错误时,例如找不到Hello或filename,execve才会返回到调用程序。其中fork调用两次,返回两次不一样,execve调用一次且从不返回。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:

int main(int argc , char **argv , char *envp);

结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello,需要以下几个步骤:

1.删除已存在的用户区域。

2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。

3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。

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

程序新开始的栈帧如下:

  

图 6.4 CSAPP 图8-13 进程地址空间

6.5 Hello的进程执行

1.进程时间片:

是指一个进程和执行它的控制流的一部分的每一时间段。

2.用户模式和内核模式:

处理器为了安全起见,不至于损坏操作系统,必须限制一个应用程序可执行指令能访问的地址空间范围。就发明了两种模式用户模式和内核模式,其中内核模式(上帝模式)有最高的访问权限,甚至可以停止处理器、改变模式位,或者发起一个I/O操作,处理器使用一个寄存器当作模式位,描述当前进程的特权。进程只有当中断、故障或者陷入系统调用时,才会将模式位设置成上帝模式,得到内核访问权限,其他情况下都始终在用户权限中,就能够保证系统的绝对安全。

3.上下文切换:

操作系统内核使用一种成为上下文切换的异常控制流来实现多任务,内核为每个进程维持一个上下文,上下文由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、和各种内核数据结构。

1. 保护当前进程的上下文。

2. 恢复某个先前被抢占的进程被保存的上下文。

3. 将控制传递给这个新恢复的进程。

  

图 6.4 CSAPP 图8-14 进程上下文切换的剖析

6.6 hello的异常与信号处理

异常:控制流的突变,用来响应某种变化。

异常分为4类:

1.中断:异步异常,来自处理器外部的I/O设备。异常处理后会执行下一条指令。

2.陷阱:同步异常,是执行系统调用函数的结果。函数调用结束后会执行下一条指令。

3.故障:同步异常,由错误情况引起,如缺页,浮点异常等等。异常处理成功则重新执行该指令,否则程序终止。

4.终止:同步异常,由致命错误造成。该异常将终止程序。

hello的运行测试:

1.正常运行:

程序正常结束并完成回收。

 

图 6.6.1 程序正常运行界面

2.Ctrl + z

这个操作向进程发送了一个sigtstp信号,让程序暂时挂起,输入ps命令符发现hello进程并没有被关闭。

 

图 6.6.2 程序运行中输入Ctrl + z

3. Ctrl + c

这个操作向进程发送了一个sigint信号,让进程直接结束,输入ps命令符发现hello进程已经被终止。

 

图 6.6.3 程序运行中输入Ctrl + c

4. ps

5. jobs

jobs指令的功能使列出暂停的进程。

 

图 6.6.5 程序运行中输入jobs

6. pstree

pstree用进程树的方法把各个进程用树状图的方式连接起来。

 

图 6.6.5 程序运行中输入pstree

6.7本章小结

本章介绍了程序在shell执行及进程的相关概念,描述了shell是如何在用户和系统内核之间建起一个交互的桥梁。程序在shell中执行是通过fork函数及execve创建新的进程并执行程序。hello程序在进程的作用下已经可以发挥程序的作用,并使得各个进程可以并发执行而不会产生冲突矛盾。


7章 hello的存储管理

7.1 hello的存储器地址空间

1.物理地址:

放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就将相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。

2.虚拟地址:

CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。

3.线性地址:

分段机制下CPU寻址是二维的地址即,段地址:偏移地址,CPU不可能认识二维地址,因此需要转化成一维地址即,段地址*16+偏移地址,这样得到的地址便是线性地址(在未开启分页机制的情况下也是物理地址)。

4.逻辑地址:

程序运行由CPU产生的与段相关的偏移地址部分,是描述一个程序运行段的地址。

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

逻辑地址是程序源码编译后所形成的跟实际内存没有直接联系的地址,即在不同的机器上,使用相同的编译器来编译同一个源程序,则其逻辑地址是相同的,但是相同的逻辑地址,在不同的机器上运行,其生成的线性地址又不相同,因为把逻辑地址转换成线性地址的公式是 线性地址 = 段基址 * 16 + 偏移的逻辑地址 ,而段基址由于不同的机器其任务不同,其所分配的段基址(线性地址)也会不相同,因此,其线性地址会不同。

即使,对于转换后线性地址相同的逻辑地址,也因为在不同的任务中,而不同的任务有不同的页目录表和页表把线性地址转换成物理地址,因此,也不会有相同的物理地址冲突。

注意的是,源码编译后生成的地址,只是偏移的地址,而形成逻辑地址的[段基址:偏移地址]中的段基址,是在生成任务时才定下来的,也就是说,[段基址:偏移地址]只有在进程中才会用到,在程序中只有偏移地址的概念。

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

分页管理机制通过上述页目录表和页表实现32位线性地址到32位物理地址的转换。控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位(即位22至位31)作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位(即位12至位21)作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。

为了避免在每次存储器访问时都要访问内存中的页表,以便提高访问内存的速度,80386处理器的硬件把最近使用的线性—物理地址转换函数存储在处理器内部的页转换高速缓存中。在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。页转换高速缓存也称为页转换查找缓存,记为TLB。

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

首先将VPN分成三段,对于TLBT和TLBI来说,优先在TLB中寻找对应的PPN,但可能出现缺页的情况,需要到页表中查找。此时,VPN被分成了更多段CR3是对应的L1PT的物理地址,进一步步递进往下寻址,越往下一层每个条目对应的区域越小,寻址越细致,在经过4层寻址之后找到相应的PPN与和VPO拼接起来。

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

得到物理地址之后,将物理地址拆分为CT(标记)+ CI(索引)+CO(偏移量),取组索引对应位,向L1cache中寻找对应组。如果存在,则比较标志位,并检查对应行的有效位是否为1。如果上述条件均满足则命中。否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后向上级cache返回直到L1cache。如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的原位置。

7.6 hello进程fork时的内存映射

mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。

vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。

在用fork创建虚拟内存的时候,要经历以下步骤:

1.创建当前进程的mm_struct,vm_area_struct和页表的原样副本。

2.两个进程的每个页面都标记为只读页面。

3.两个进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制。

7.7 hello进程execve时的内存映射

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

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

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

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

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

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

情况1:段错误:

首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)。

情况2:非法访问:

接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。

情况3:

如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入新的页面并更新到页表。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

1.隐式空闲链表:

空闲块通过头部中的大小字段隐含地连接着。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。

①放置策略:首次适配、下一次适配、最佳适配。

首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。

②合并策略:立即合并、推迟合并。

立即合并就是在每次一个块被释放时,就合并所有的相邻块;推迟合并就是等到某个稍晚的时候再合并空闲块。

2.显式空闲链表:

每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表使首次适配的时间减少到空闲块数量的线性时间。

空闲链表中块的排序策略:

一种是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处,另一种方法是按照地址顺序来维护链表,链表中每个块的地址都小于它后继的地址。

分离存储:维护多个空闲链表,每个链表中的块有大致相等的大小。将所有可能的块大小分成一些等价类,也叫做大小类。

分离存储的方法:简单分离存储和分离适配。

7.10本章小结

本章介绍了存储器的地址空间,讲述了系统虚拟地址,物理地址,线性地址,逻辑地址概念以及相互之间的关系,动态内存分配的不同策略。本章还讲解了访问内存过程中如何处理却页故障,以及动态存储分配的不同方式。


8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

文件的类型:

1.普通文件(regular file):包含任意数据的文件。

2.目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件(“文件夹”)。

3.套接字(socket):用来与另一个进程进行跨网络通信的文件

4.命名通道

5.符号链接

6.字符和块设备

设备管理:unix io接口

1.打开和关闭文件

2.读取和写入文件

3.改变当前文件的位置

8.2 简述Unix IO接口及其函数

1.打开文件:

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

open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符。

2.关闭文件:

int close(fd);

fd是需要关闭的文件的描述符,close返回操作结果,关闭一个已关闭的描述符会出错。

 

3.读取文件:

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

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

4.写入文件:

ssize_t wirte(int fd,const void *buf,size_t n)

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

5.复制文件:

int dup2(int oldfd, int newfd);

dup2函数复制描述符表表项oldfd到描述符表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。

6.改变文件位置:lseek()函数。

8.3 printf的实现分析

printf函数主要调用了vsprintf和write两个外部函数。

 

图 8.3.1 printf函数的函数体

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

图 8.3.2 vsprintf函数的函数体

printf的运行过程:

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

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

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

8.4 getchar的实现分析

 

图 8.4 getchar函数的函数体

getchar调用了一个read函数,这个read函数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。

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

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

8.5本章小结

本章介绍了Linux unix I/O设备管理机制,了解了其接口和函数,简单分析了printf和getchar函数的实现方法。

 

 

 

 

 

 

 

 

 

 

 

 

结论

hello的程序人生经历了诞生执行再到消亡的过程:

1. 源文件的编写:确定程序内容,写入hello.c中,生成最初始源文件。

2. 预处理:预处理去对hello.c进行预处理过程生成hello.i预处理文件,将源程序中使用到的外部库展开导入程序中。

3. 编译:将预处理后的hello.i文件进行程序语法分析优化生成hello.s汇编文件。

4. 汇编:将汇编文件hello.s翻译成二进制机器更易读懂的机器代码hello.o。

5. 链接:使用链接器将hello.o与其他程序中使用到的库函数文件进行合并链接,生成可执行文件hello。

6. 运行程序:在终端中输入运行命令,shell进程调用fork为hello创建子程序,随后调用execve启动加载器,加映射虚拟内存。

7. 执行指令:CPU为程序分配时间片,在一个时间片中hello使用CPU资源顺序执行控制逻辑流。

8. 内存使用: MMu将程序中使用的虚拟内存地址通过页表映射成物理地址, printf会调用malloc向动态内存分配器申请堆中的内存。

9. 异常处理:hello执行的过程中可能收到来自键盘输入的信号,收到相应信号后调用信号处理程序进行处理。

10. 进程结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。

感悟:

hello的一生过程包含了计算机系统内部一系列底层机制,将hello.c一步一步变成了hello这样一个完美的可执行文件,最后在bash和os的处理下消失在进程的海洋当中。在hello的一生的学习和梳理之中,深入理解了计算机系统内部各部分之间的分工与协作。

在贯穿大作业的始终,回顾了整个计算机系统课程学习的课程内容,也复习加深了前置实验中使用过的工具,对整个系统和程序运行过程有了更深入的认识与理解,对Linux系统的操作也更加熟悉与熟练。
附件

文件名称

文件描述

hello.c

hello的c语言代码(源文件)

hello.i

预处理后的文件

hello.s

编译后产生的汇编文件

hello.o

可重定位的目标文件

hello

可执行文件


参考文献

[1] 龚奕利. 深入理解计算机系统.北京:机械工业出版社,2016.

[2] 预处理_百度百科:

https://baike.baidu.com/item/预处理/7833652?fr=aladdin

[3] 编译_百度百科:

https://baike.baidu.com/item/编译/1258343?fr=aladdin

[4] atoi_百度百科:

https://baike.baidu.com/item/atoi/10931331?fr=aladdin

[5] 汇编程序_百度百科:

https://baike.baidu.com/item/汇编程序/298210?fr=aladdin

[6] 动态链接原理分析:

https://blog.csdn.net/shenhuxi_yu/article/details/71437167

[7] getchar函数浅谈:

https://blog.csdn.net/zhuangyongkang/article/details/38943863

[8] printf函数实现的深入剖析:

https://www.cnblogs.com/pianist/p/3315801.html

[9] 逻辑地址、线性地址和物理地址之间的转换:

https://blog.csdn.net/gdj0001/article/details/80135196

 

 

标签:预处理,文件,程序,Hello,地址,人生,P2P,进程,hello
来源: https://blog.csdn.net/qq_35425435/article/details/118272340

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有