计算机系统

大作业

题     目 程序人生-Hellos P2P

专       业 人工智能(未来技术)

学     号 120L020301

班   级 2036011

学       生 张思远

指 导 教 师 刘宏伟

计算机科学与技术学院

2022年5月

摘  要

本文介绍了hello的一生。首先,按照编译器驱动程序的处理顺序,分析了hello.c的变化,它历经预处理、编译、汇编、链接,脱胎换骨,从人类的造物(高级语言程序)变成机器的宠儿(可执行目标文件)。这就是所谓P2P的过程,但是,和人类世界的P2P不同,计算机程序的P2P一般不会暴雷。之后我们分析了hello的运行过程,从进程、内存、IO三个角度品味它的人生。最后hello被回收,归于虚无,即020。

关键词:hello程序;编译;链接;进程;内存;I/O管理                            

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.2.1 硬件环境

1.2.2 软件环境

Windows10专业版 64位VM VirtualBox 6.1.32;Ubuntu 20.04.4;

1.2.3 开发工具

Visual Studio 2022 64位;Notepad++&gcc

1.3 中间结果

1.4 本章小结

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

表1:常用的预处理指令

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1数据类型

3.3.2赋值

3.3.3关系操作

3.3.4算术操作

3.3.5数组/指针/结构操作

3.3.6控制转移

3.3.7函数调用:

3.4 本章小结

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.4.1 机器语言的构成与汇编语言的映射关系

4.4.2从汇编语言转换成机器语言的过程中的不一致的情况:

4.5 本章小结

(第4章1分)

5 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.3.1 ELF头

5.3.2 节头部表

5.3.3 程序头部表:

5.3.4 符号表

5.3.5 重定位节

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.7.1 PLT表

5.7.2 GOT表:

5.8 本章小结

(第5章1分)

6 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

1. 保存当前进程的上下文

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

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

6.6 hello的异常与信号处理

6.6.1 可能出现的异常(包括中断,陷阱,故障,终止)

6.6.2 信号及其处理

6.7本章小结

(第6章1分)

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本章小结

(第7章 2分)

8 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

1.open()函数

2.close()函数

3.read()函数

4.write()函数

5.lseek()函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

(第8章1分)

结论

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

附件

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

参考文献

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

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

第1章 概述

1.1 Hello简介

1.1.1 P2P

P2P是指Hello从Program变为Process的过程。在这一阶段,Hello从源文件经过预处理,编译,汇编,链接四个阶段成为二进制可执行目标文件,然后被分配进程执行。

Program被处理的全过程:编译器驱动程序依次调用预处理器、编译器、汇编器和链接器处理程序文件。首先,C预处理器(cpp)完成宏替换,文件包含,源程序被预处理为一个中间文件(仍为文本文件);然后C编译器(cc1)生成我们耳熟能详的汇编文件(用低级语言写成的文本文件);之后,汇编器(as)将汇编语言文件翻译成可重定位目标文件(一个未链接的二进制文件);最后链接器(ld)生成我们需要的可执行目标文件Hello。

之后,shell创建新进程,加载Hello运行需要的上下文,内核开始调度此进程执行,Hello正式从纸面上的Program变成了一个活动的Process。

1.1.2 020

CPU为hello进程分配时间片,内核调度各个进程的执行(计算机上肯定不可能只有Hello独占CPU)。当内核挂起其他进程后,它把控制移交给Hello,CPU执行它的逻辑控制流,按照程序的请求,执行一般指令,进行系统调用或执行其他函数。运行结束后,shell收到信号,作为父进程回收僵死的hello子进程,操作系统内核删除hello的相关状态,一切归于平静,即020,生不带来死不带去。

(我们将O2O写成020,因为是Zero to Zero)

1.2 环境与工具

1.2.1 硬件环境

x64 CPU;2.60GHz/2.59GHz;16G RAM;256GHD Disk。

1.2.2 软件环境

Windows10专业版 64位;VM VirtualBox 6.1.32;Ubuntu 20.04.4;

1.2.3 开发工具

Visual Studio 2022 64位;Notepad++&gcc

1.3 中间结果

hello.i:C预处理器产生的一个ASCII码的中间文件,用于分析预处理过程。

hello.s:C编译器产生的一个ASCII汇编语言文件,用于分析编译的过程。

hello.o:汇编器产生的可重定位目标程序,用于分析汇编的过程。

hello:链接器产生的可执行目标文件,用于分析链接的过程。

asm.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。

asm2.txt:hello的反汇编文件,用于分析可执行目标文件hello。

helloelf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。

helloelf2.txt:hello的ELF格式,用于分析可执行目标文件hello。

1.4 本章小结

本章从要求的P2P和020(事实上是硬件和操作系统层面)介绍了hello程序从编译到运行的整个过程,列举了进行实验的软硬件环境及使用的基本开发工具,并提供了实验过程中文章述及的全部中间文件。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理是指在编译之前对程序中的特殊命令进行的处理工作。以#开头的是C语言的编译预处理命令。

预处理主要有以下三个任务需要完成:

1.文件包含,源程序开头诸如#include "文件名"的语句告诉我们程序需要这些头文件才能运行,预处理阶段将引入这些头文件,并直接在它们对应的include处执行替换;

2.条件编译,预处理器决定哪些代码被编译,而哪些不被编译,通常把排除在外的语句换成空行。

格式示例:

#if/#ifdef 表达式

程序段1

#else

程序段2

#endif

  1. 宏替换,将程序中所用到的宏替换为宏定义的替换文本。注意这种只是简单的文本语言上的等效替换,预处理阶段不会为表达式求值,因此宏中的计算语句是不能生效的。

格式示例:

#define 宏名 字符串

指令

说明

#

空指令,无任何效果

#include

包含一个源代码文件

#define

定义宏

#undef

取消已定义的宏

#if

如果给定条件为真,则编译下面代码

#ifdef

如果宏已经定义,则编译下面代码

#ifndef

如果宏没有定义,则编译下面代码

#elif

如果前面的#if给定条件不为真,当前条件为真,则编译下面代码

#endif

结束一个#if……#else条件编译块

表1:常用的预处理指令

2.2在Ubuntu下预处理的命令

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

预处理效果:生成hello.i文件,被与hello保存在同一个文件夹Downloads下。

图2.2 预处理指令

2.3 Hello的预处理结果解析

图2.3.1 预处理结果

图2.3.2 预处理结果

1.首先我们可以看到,main函数的主体部分几乎没有任何改变,依然是一个具有很好阅读性的高级语言程序,这也佐证了对预处理阶段的定义“对源程序中的伪指令(以#开头的指令)和特殊符号进行处理”。

2.另一方面,对比发现hello.i文件中加入了大量不属于源程序本身的的代码语句,使得hello.i程序变得极其庞大,约有3000+行。这是因为预处理器将预处理指令#include 替换为了系统头文件stdio.h ,unistd.h,stdlib.h中的内容,加入了外部函数的声明,数据类型的定义等内容。

3.特别注意到一个有意思的现象:预处理阶段删除了所有的程序注释,这一点确实在教科书上未曾提及,但是很有意思。

2.4 本章小结

预处理的本意是简化程序员的工作,使程序有更好的可移植性:通过宏定义,我们可以方便直观的替换变量;通过#if等编译命令,我们可以增强代码的移植性和泛用性。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译是编译器(ccl)将预处理后的文本文件hello.i翻译成汇编语言文本文件hello.s的过程,汇编包括词法分析,语法分析,代码优化等阶段,最终生成目标代码。

在进行词法分析时,编译器对由字符组成的单词进行处理,把源程序中的单词转换为单词符号,源程序变为所谓的中间程序。

在进行语法分析时,编译器将按语法规则分析检查每条语句的逻辑结构,如果错误将返回报错,如果每个语法单位都合法,就生成目标代码。

编译器在一般情况下还会自动为程序选择保守但有效的优化,除非优化选项被禁用。

编译的作用:

编译的最重要作用是将高级语言程序转化为统一的汇编语言程序,以便进一步进行后续的汇编,链接等操作,编译得到的汇编文件是源文件与二进制可执行文件间的重要纽带。

3.2 在Ubuntu下编译的命令

编译用命令(在已经生成hello.i的前提下):gcc -S hello.i -o hello.s

编译效果:生成hello.s文件,被与hello.i保存在同一个文件夹Downloads下。

图3.2 编译指令

3.3 Hello的编译结果解析

编译得到的汇编文件已经被存储在了hello.s中,下面我们将按照要求,分析实例来说明编译器是怎么处理C语言的各个数据类型以及各类操作的。我们将从数据、赋值、关系操作、算数操作、数组/指针/结构操作、控制转移和函数操作这七个方面结合汇编代码予以详细的解释和说明。

3.3.1数据类型

  1. 单个局部变量:让我们先从最简单的入手。注意程序中作为循环计数变量的i,我们知道本地非静态局部变量应该是储存在用户栈上或者是寄存器里。i被存放在栈上%rbp-4的位置(rbp存放原栈顶的那一帧),符合我们的学习结论。推测i不在寄存器里的原因是后面对argv[]的访问占用了大部分寄存器。

图3.3.1.1 局部变量相关汇编代码

  1. 常量:它们保存在只读代码段,具体来说,数字常量在.test节,字符串常量在.rodata节。

数字常量的存储位置体现的不典型,例如考虑语句cmpl $7, -4(%rbp),其中的7就是一个数字常量,作为循环终止的测试条件。为了展示他们的存储位置,我们以字符串常量为例,如图:

图3.3.1.2 常量相关汇编代码

puts函数把他的地址作为一个参数,如下:

3.3.2赋值

这个程序中的赋值操作并不多。我们重点分析对循环计数变量i的赋值操作。由下图可以看到对i=0的赋值在汇编代码中体现为“movl $0,-4(%rbp)”。编译器通过双字传送操作movl把常数0赋值给i,注意i是一个四字节变量,恰是一个x86/64架构下定义的双字。

图3.3.2 赋值相关汇编代码

3.3.3关系操作

关系操作就是用比较运算符对操作数进行关系运算,一般返回TRUE或FALSE。在本程序中,关系操作为“argc!=4”和“i<8”两个表达式。

对于argc!=4,它判断传入的参数个数是否为4,并在相等时才进行下一步的跳转。注意cmpl指令会设置条件码标志位ZF,其汇编代码如下:

图3.3.3 关系相关汇编代码

对于i<8,同样是进行比较并设置条件码,不过我们要同时关注条件码标志位ZF和SF,他们的用处我们将在控制转移时予以再次分析。

3.3.4算术操作

算术运算指令

汇编语言中的add、sub、adc、sbb、inc、dec、cmp、imul、idiv、aaa等都是算术运算指令,这些指令实现寄存器和内存中的数据以及立即数的算数运算。hello.c中出现的算术操作为for循环中的i++,汇编代码中体现为addl指令(对两个整型操作数求和),它把立即数1加到栈上的循环计数变量i上,代码如下图所示:

图3.3.4 算术相关汇编代码

3.3.5数组/指针/结构操作

在本汇编程序中出现的非基本数据类型主要是传入参数数组argv[]。hello.c程序在调用printf()函数时,将argv数组中的第二个和第三个元素作为函数的参数传入,它们分别是两个常量字符串的地址,而按照约定俗成的惯例,argv[0]是可执行文件名,argv[3]中存放一个空指针NULL,表示指针数组已到末尾。

我们知道,数组是物理上邻接的存储单元,相邻的元素拥有相邻的地址。在汇编语言中,对数组(包括一般结构体)以基址+偏移量的形式进行访问。对于本程序,argv[0]的地址存放在了栈-32(%rbp)位置处,之后存在%rax中,因为字长为8,argv[1]和argv[2]在(%rax+16)和(%rax+8)中,他们作为参数%rdi和%rsi被提供给标准输出函数printf。

图3.3.5 数组相关汇编代码

3.3.6控制转移

这里我们主要分析控制转移的跳转指令部分,因为比较操作已经在前面分析过,它们为跳转语句设置了条件码。注意je需要的是ZF,而jle需要的则是(SF^OF)| ZF。je出现在条件判断if的分支语句中,这里判断传入参数的个数,如果为2(体现为参数向量argv长为4),就进入L2,否则puts输出提示信息表明格式有误。其代码如下:

图3.3.6 控制转移相关汇编代码

jle出现在循环的判断语句中,首先i被赋值为0,然后跳转到cmpl指令,与7进行比较,如不大于7则执行大括号中的循环体,跳转到L4的指令部分,L4的最后一条语句中更新了循环计数器,将其值加1。然后再次进入L3做循环条件判断,与7进行比较直至其大于7跳出循环进行后续操作。可以看到这里控制转移依赖cmp的测试与jle的跳转实现。其代码如下:

图3.3.6 控制转移相关汇编代码

3.3.7函数调用:

1.函数调用的一般过程:

首先是返回地址的压栈和控制的传递,父函数使用call指令(特别的,main的父函数是系统启动函数,他先构造初始的用户栈),将返回地址(一般是下一条指令的地址)入栈,然后跳转到被调用函数的起始地址,如需要参数传递,第一个参数保存在%rdi中,第二个参数保存在%rsi中,其他依次保存在%rdx,%rcx,%r8,%r9中(最多六个),若有更多参数则保存在栈中。被调用函数开始执行,并为自己开辟新的栈帧,完毕后通过ret指令返回,返回值保存在%rax,弹出返回地址交还控制。

2.main函数调用:其传入参数argc和argv[],分别用寄存器%rdi和%rsi存储,汇编代码如下:

图3.3.7 函数相关汇编代码

  1. exit函数调用

图3.3.7 函数相关汇编代码

  1. getchar函数调用

图3.3.7 函数相关汇编代码

  1. atoi函数调用

图3.3.7 函数相关汇编代码

  1. sleep函数调用

图3.3.7 函数相关汇编代码

  1. printf函数调用

我们对相对来说参数较多的printf来分析一下:

movq     -32(%rbp), %rax

addq      $16, %rax

movq     (%rax), %rdx

这三行把**argv减去16得到argv[2]的地址,再将argv[2]传给%rdx。

movq     -32(%rbp), %rax

addq      $8, %rax

movq     (%rax), %rax

movq     %rax, %rsi

这几行像上面一样将argv[1]传给%rsi。

movl      $.LC1, %edi

这个指令将字符串常量"Hello %s %s\n"传给%edi。

movl      $0, %eax

这个指令将%eax清0。

call printf

调用printf,此时函数需要的参数均已被构造好。

图3.3.7 函数相关汇编代码

  1. puts函数调用

图3.3.7 函数相关汇编代码

3.4 本章小结

计算机无法直接识别并执行高级语言,只能执行机器指令。汇编语言是他们二者间的纽带。而从高级语言到汇编语言的这项重要的工作由编译器执行。本章介绍了编译的概念和作用,并详细地分析了编译器如何处理C语言的各种数据以及各类操作。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:

汇编是指编译器驱动程序调用汇编器(as)将汇编语言文本文件文件翻译成成机器语言的可重定位目标文件,生成的也就是我们的hello.o文件(后缀名.o表示二进制可重定位文件),这是一个机器语言编写的二进制文件。

作用:

汇编是将汇编语言转换成最底层的、机器可理解的机器语言的过程。这句话包含两层意思。首先,汇编只是转换的过程,不是结果。本质上,汇编代码仍然无法被机器读懂,需要链接操作进行符号解析和重定位。其次,汇编是不可或缺的中间步骤,其作用是将汇编指令转换为机器指令,使之在链接后能够被计算机直接执行,而链接本身并不执行这种转换。

4.2 在Ubuntu下汇编的命令

汇编命令(在已经生成hello.s的前提下):

gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

汇编结果:生成hello.o文件,被与hello.s保存在同一个文件夹Downloads下。

图4.2 汇编指令

4.3 可重定位目标elf格式

hello.o已经是一个二进制文件,直接用文本查看工具是无法打开的。我们在命令行键入readelf命令:

readelf -a hello.o > helloelf.txt

4.3.1查看hello.o的ELF格式,并将结果重定向到helloelf.txt便于查看分析。如下图:

图4.3 readelf指令

一个典型的elf格式的可重定位目标文件以ELF头开始,还包括代码区、数据区,以下是具体形式:

ELF头

.text(已编译程序的机器代码)

.rodata(只读数据段)

.data(已初始化且不为0的全局和静态C变量)

.bss(未初始化的全局和静态C变量,初始化为0的全局或静态变量)

.symtab(程序中定义和引用的函数和全局变量信息的符号表)

.rel.text(.text节的位置列表)

.rel.data(全局变量的重定位信息)

.debug(调试符号表)

.line(C源程序与.text指令之间的映射)

.strtab(字符串表)

节头部表

4.3.2 ELF头

ELF头以一个16字节的序列开始,就是图中的Magic序列7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00。这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。

ELF头的声明如下:

typedef struct {

unsigned char   e_ident[EI_NIDENT];

Elf64_Half      e_type;

Elf64_Half      e_machine;

Elf64_Word      e_version;

Elf64_Addr      e_entry;

Elf64_Off       e_phoff;

Elf64_Off       e_shoff;

Elf64_Word      e_flags;

Elf64_Half      e_ehsize;

Elf64_Half      e_phentsize;

Elf64_Half      e_phnum;

Elf64_Half      e_shentsize;

Elf64_Half      e_shnum;

Elf64_Half      e_shstrndx;

} Elf64_Ehdr;

ELF头如图,可见其中包含的信息已经被以英文显示。

图4.3.2 ELF头

4.3.3 节头部表

在 ELF 文件中可以包含很多节(Section),所有这些节都登记在一张称为节头部表(Section Header Table)的数组里。节头表的每一个表项是一个Elf64_Shdr结构,通过每一个表项可以定位到对应的节。Hello.s对应的节头部表如下:

图4.3.3 节头部表

4.3.4 重定位节

汇编器遇到对最终位置未知的目标引用,会产生一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位信息在重定位节.rel.text中,已初始化数据的重定位条目放在.rel.data中。链接器将会通过重定位条目计算出正确的地址。我们由下图可以看到,hello.o需重要定位的有:.rodata中的模式串,puts,exit,printf,atoi,sleep,getchar等函数符号,由于程序没有需要重定位的数据条目,.rel.data似乎缺省。

图4.3.4 重定位节

4.3.5 符号表

符号表存放程序中定义和引用的函数和全局变量的信息,特别注意非静态全局变量不会存放在符号表中。符号表如下图:

图4.3.5 符号表

对于函数main,Ndx=1表明它在.text节,value=0表明它在.text节中偏移量为0的地方,size=146表明大小为146字节,bind=GLOBAL表明它是全局符号,type=FUNC:表明它是函数。对于外部库函数,它们的位置需要链接后才能确定,因而Ndx=UND。

4.4 Hello.o的结果解析

我们使用指令objdump -d  hello.o >asm.txt,把反汇编结果输出到文本文件asm.txt中进行分析。如下图:

图4.4 反汇编文件

4.4.1 机器语言的构成及与汇编语言的映射关系:

由汇编的过程我们可以知道,汇编指令被唯一的映射到二进制的机器语言,这种映射是无歧义的。因为不同的汇编指令被映射到不同的二进制功能码(包括ifun和icode),操作数被映射成对应的二进制表示,比如由上图可以观察到机器码e8即是对应call指令。因此汇编指令与机器指令之间是双射,二者可以互相唯一确定对方(注意,这里的汇编指令是指确定了操作数类型的汇编指令,否则双射并不成立,例如从寄存器到内存的传送与从寄存器到寄存器的传送拥有不同的二进制码)。

4.4.2从汇编语言转换成机器语言的过程中的不一致的情况:

  1. 对于函数调用的代码来说,call指令后从函数名变成了被调函数的PC相对地址。但很不幸,大多数函数调用都是外部库函数,因而它们的地址没有被填入,它们需要重定位,并将在链接之后被填写正确的位置,因此call指令后的二进制码为全0,表示调用函数地址与下一条指令地址偏差为0,这其实是因为函数地址未确定。此外,注意到对常量字符串(存储在.rodata节)的访问,hello.o的反汇编中使用0x0(%rip),默认值为0,重定位后将更新为字符串的实际值。

图4.4.2.1 常量字符串的0地址

  1. 机器语言中的条件转移跳转使用的是确定的地址,而汇编语言使用的是诸如.L0,.L1之类的助记符。

图4.4.2.2 跳转的变化

  1. 在相关数字的表示上,hello.s中的操作数为十进制表示法,然而反汇编的结果中操作数为十六进制,如汇编指令中的-32被翻译为-0x20。这是因为反汇编是对二进制文本的直接翻译。

图4.4.2.3 数进制变化

  1. 反汇编文件中增加了一些注释,如# 20 <main+0x20>。

4.5 本章小结

本章,我们通过汇编,将汇编语言文件hello.s翻译成机器语言文件hello。我们分析了hello.o与hello.s的相同与不同之处,学会了分析ELF文件,弄清楚了汇编语言与机器语言的异同,同时我们对汇编未完成的重定位有了认识,链接便提上了日程。

(第4章1分)

5 链接

5.1 链接的概念与作用

链接的概念:

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

1.编译时,就是源代码被编译成机器代码时(静态链接器负责);

2.加载时,也就是程序被加载到内存时(加载器负责);

3.运行时,由应用程序来实施(动态链接器负责)。

链接包括两个主要任务:符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来,然后修改所有对符号的引用)。

链接的作用:

链接是十分重要,不可或缺的,在软件开发中扮演着一个关键的角色,因为它使得分离编译成为可能。无需将一个大型的应用程序组织成一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块,极大地提高了大型程序编写的效率。

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

链接的结果:生成可执行文件hello,和其他文件一起保存在Downloads文件夹下。如下图:

图5.2 链接指令

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

使用readelf命令readelf -a hello > helloelf2.txt查看可执行目标文件hello的ELF格式,并将结果重定向到helloelf2.txt,之后我们的分析均针对这个文本文件。

图5.3 readelf指令

5.3.1 ELF头

ELF头以Magic序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。下图的序列描述了系统的字的大小为8字节,字节顺序为小端序。

ELF头的其他部分包含了ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等信息,如下图所示。

图5.3.1 ELF头

5.3.2 节头部表

在 ELF 文件中包含很多节,这些节都登记在一张称为节头部表的数组里。与hello.o相比,hello的节头部表一共描述了26个不同节信息,比hello.o多出13个。各节的起始地址由偏移量给出,同时也给出了大小等信息。

图5.3.2 节头部表

5.3.3 程序头部表:

为链接器提供运行时的加载内容和提供动态链接的信息,每一个条目包含各段在目标文件中的偏移、在虚拟地址空间中的位置、目标文件中的段大小、内存中的段大小、运行时访问权限和对齐方式。

图5.3.3 程序头部表

5.3.4 符号表

符号表存放程序中定义和引用的函数和全局变量的信息。它的条目value为距定义目标的节的起始位置的偏移,size为目标的大小,type为符号类型,bind表示符号是本地的还是全局的。hello的符号表中符号增加了,因为一些库函数和启动函数符号被加入,下图为符号表(51个符号)。

图5.3.4 符号表

5.3.5 重定位节

注意到链接后已经完成了对.rel.text的重定位,因此它没有出现在ELF文件中。但hello中又出现了6个新的重定位条目。他们都是库函数,因为此时只是静态链接,程序未能加载和运行,因此还没有进行动态链接,共享库中函数需要重定位节,在动态链接后才能确定地址。

图5.3.5 重定位节

5.4 hello的虚拟地址空间

使用edb加载hello,在左下角Data Dump处可以看到进程的虚拟地址空间各段信息。可以看出,段的虚拟空间从0x401000开始,到0x401ff0结束,与课本所述0x400000不同,推测是使用了ASLR(Address-Space Layout Randomization,地址空间布局随机化)。

图5.4 虚拟内存地址空间

5.3中的节头部表告诉了我们节偏移量,我们可以以此计算每个节被映射到的虚拟内存中的位置。

例如条目

  1. .text             PROGBITS         00000000004010f0  000010f0

告诉我们.text节的起始地址为0x4010f0,节偏移为10f0,在edb中查看这个位置如图:

图5.4 虚拟内存地址空间

发现该地址处正好是代码中的第一条指令0: f3 0f 1e fa           endbr64

再例如条目

  1. .init             PROGBITS         0000000000401000  00001000

图5.4 虚拟内存地址空间

也在edb中有着对应的内存位置。

5.5 链接的重定位过程分析

我们使用命令objdump -d -r hello >asm2.txt对hello进行反汇编,并将结果重定向到asm2.txt,相关截图如下:

图5.5.1 反汇编指令及其结果

hello与hello.o的不同主要体现在以下几点:

  1. 首先从虚拟地址方面:可执行文件hello的起始代码段地址已经是从0x400000开始的虚拟内存地址了,而可重定位目标代码hello.o的代码段起始位置为0x0,表明它还没有被映射到虚拟内存。
  2. 在hello.o中,只有main函数的汇编指令,这是因为其他外部库函数没有被重定位,程序中没有他们的信息;而在hello中,重定位完成,引入了其他库的各种数据和函数,以及一些必需的启动/终止函数,因此汇编指令数量大大扩充。
  3. main函数中涉及重定位的指令的二进制代码被修改。汇编器遇到对最终位置未知的目标引用,会产生一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。在链接的过程中,链接器会根据重定位条目以及已知的最终位置对修改指令的二进制码,这个过程就是重定位的过程。

重定位过程分析:

我们以getchar()函数为例子,进行对重定位过程的分析。了解了重定位条目的信息,我们只需要知道引用的运行时的地址,即:refaddr = ADDR(s)+r.offset ,其中ADDR(s)=0x401125,又已分析得到r.offset为0x87,可以计算refaddr为0x4011ac,这是重定位地址。于是*refptr= (unsigned)(ADDR(getchar)+r.addend- reffaddr)=0xff ff 00,这样我们就得到了重定位引用的十六进制值。结果与下图的结果一致:

图5.5.2 重定位过程

5.6 hello的执行流程

用edb的analyze功能,分别在ld-xxx.so和hello区域analyze一次,在终端会出现如下信息,可以发现动态链接库中依次出现了如下函数:

图5.6 hello的执行

main调用的函数依次是:

图5.6 hello的执行(续)

5.7 Hello的动态链接分析

当程序调用一个由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。但这需要链接器修改调用模块的代码段,GNU编译系统使用延迟绑定将过程地址的绑定推迟到第一次调用该过程时。

延迟绑定通过两个数据结构之间的交互实现,分别是GOT(数据段)和PLT(代码段)。

5.7.1 PLT表

PLT是一个数组,每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。从PLT[1]开始的每个条目对应一个调用的库函数,因为它在代码段,我们可以直接在反汇编代码中查看它。

图5.7.1 PLT表

5.7.2 GOT表:

GOT是一个数组,每个条目为8字节地址。GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息,GOT[2]是动态链接器在1d-linux.so模块中的入口点。GOT[3]开始的每个条目对应于一个被调用的函数。

我们运行edb,在edb的plugin中找到symbol viewer,查找hello!.got表项。

发现一个条目hello!.got+0x10,我们把这个地址减去0x10,查看它的地址,发现这正是GOT表的起始位置。

图5.7.2 GOT表

查看GOT[0],发现这正是与动态链接器相关的地址。

图5.7.2.1 GOT[0]

我们知道,GOT[3]开始为调用函数的地址,而每个表项为8字节,验证该结论,在symbol viewer中查找该地址,发现正好就是puts在内存中的位置。依次可以验证后面的几项是printf、getchar等。这说明在hello的_start开始前调用了动态链接的相关函数,修改GOT表的内容,将其指向库中函数的运行时位置。

图5.7.2.2 GOT[3]

5.8 本章小结

在本章,我们通过ELF文件和edb分析链接的过程。链接是得到可以运行的可执行目标文件前的最后一步操作。链接完成符号解析和重定位,导入外部库。链接之后,我们最终得到了可执行目标文件hello。

(第5章1分)

6 hello进程管理

6.1 进程的概念与作用

概念:

进程是计算机科学中最深刻、最成功的概念之一。进程的经典定义就是一个执行中程序的实例。系统中的每个程序都在运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的,包括存放在内存中的程序的代码和数据,它的运行时栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。

作用:

进程是一个程序的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。当我们说进程活动,指它正在运行;说它停止,指它被挂起,不会活动,也不会在状态改变前被内核调度而开始活动;说它终止,指它运行结束,不会再开始活动,终止的进程成为僵死进程,需要父进程回收以清除它对某些存储资源的占用。

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

作用:

Linux 系统中,Shell 是一个交互型应用级程序,代表用户运行其他程序(它是命令行解释器,以用户态方式运行的终端进程)。

基本流程:

其基本功能是解释并运行用户的指令,重复如下处理过程:

(1)终端进程读取用户由键盘输入的命令行。

(2)分析命令行字符串,获取命令行参数,并构造传递给 execve 的 argv 向量

(3)检查第一个(首个、第 0 个)命令行参数是否是一个内置的 shell 命令

(3)如果不是内部命令,调用 fork( )创建新进程/子进程

(4)在子进程中,用步骤 2 获取的参数,调用 execve( )执行指定程序。

(5)若用户未要求后台运行,则shell等待作业终止后将其回收。否则将进程转入后台运行,开始等待用户输入下一个命令。

6.3 Hello的fork进程创建过程

当用户在shell中输入./hello 120L020301 张思远 2,shell构造argv,发现hello不是一个内置命令,就会把它解析为一个可执行文件,通过调用fork()函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,包括代码段、数据段、共享库以及用户栈(特别注意子进程也共享父进程的信号阻塞集合,这可能带来一些问题)。子进程还获得与父进程打开文件描述符相同的一份副本,使得子进程可以读写父进程中打开的文件,父进程和子进程的不同在于他们的PID不同。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。

6.4 Hello的execve过程

shell使用fork()创建一个子进程之后,在子进程中调用exceve()函数能够在当前进程的上下文中加载并运行我们需要的hello程序。execve函数原型为execve(file*filename,),它加载并运行filename,默认filename是一个可执行文件,且带参数列表argv和环境变量envp,传入的后两个参数都是对应指针数组的首地址(二级指针)。execve从不返回,除非发生某些错误,才会返回到调用程序。

execve函数的具体动作为以下几个:

(1)删除已存在的用户区域。首先把fork()复制的用户地址副本删除,即删除当前进程虚拟地址的用户部分中的已存在的区域结构。

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

(3)映射共享区域。如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器。最后设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

特别的,在映射栈区域时,系统启动函数初始化的栈从栈底到栈顶依次为:

  1. 环境变量字符串;
  2. 参数字符串;
  3. 环境变量指针数组;
  4. 参数指针数组;
  5. 系统启动函数的栈帧;
  6. main函数未来的栈帧;

图6.4 初始栈

6.5 Hello的进程执行

操作系统使用被称为上下文切换的异常控制流来实现多任务进程运行。所谓上下文就是一个进程运行所需的状态,包括寄存器信息,栈和各种内核数据结构。操作系统内核可以交替的执行进程的控制流,并发的执行多个程序进程,一个进程执行它的控制流的一部分的每一时间段叫做时间片。在程序执行的某些时刻,内核可以结束当前进程,并重启之前的进程,这就被称为调度内核调度一个进程时,需要:

1. 保存当前进程的上下文

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

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

hello的进程调度过程:

在调用sleep函数之前,如果hello不被抢占,则hello顺序执行。hello初始运行在用户模式,sleep后被挂起进入内核模式,内核不等待休眠的hello,决定调度进程。计时器开始计时,将控制转移给其他进程,当sleep时间结束后,hello从等待队列中加入到运行队列,成为就绪状态,等到内核决定再次调度hello,hello进程就可以继续执行,直到它终止或者被停止而再次从就绪队列中移除。

图6.5 进程调度

(进程切换示意图,图源课本)

6.6 hello的异常与信号处理

6.6.1 可能出现的异常(包括中断,陷阱,故障,终止)

hello执行过程中有可能发生中断,这是因为如果其他进程使用了外部I/O设备,那么I/O信号可能在hello运行时被发送给处理器,hello进程运行时就可能会出现外部I/O设备引起的中断,此时会调用中断处理程序处理,然后交还控制给hello的下一条指令。

hello执行过程中系统调用sleep,产生陷阱。此时控制传递给适当的异常处理程序,处理程序解析参数,调用内核程序挂起hello。处理程序返回时,将控制返回给下一条指令。

hello执行过程中可能发生故障,此时调用故障处理程序处理,能够恢复就会交还控制给当前指令,否则进程终止。

hello执行过程中可能发生严重错误,此时可能引发终止,控制传递给终止处理程序,终止引起错误的程序。

6.6.2 信号及其处理

在程序执行过程中,如果乱按键盘,会显示相应的字符,如果输入的字符中含有回车键,则程序结束后getchar函数直接读取字符并自动调用。

6.6.2.1 Ctrl-c指令:在键盘上键入ctrl-c指令后,一个SIGINT信号被发送给前台进程组的每个进程,默认情况下,前台进程终止。

6.6.2.2 Ctrl-z指令:在键盘上键入ctrl-z指令后,一个SIGTST信号被发送给前台进程组的每个进程,默认情况下,前台进程停止。

图6.6.2.1-6.6.2.2 信号处理

6.6.2.3 键入ps命令可以查看当前所有进程以及它们的PID。

6.6.2.4 键入jobs命令可以查看当前的作业,可以发现hello作为唯一的作业被挂起。

6.6.2.5 pstree命令显示树形进程图

图6.6.2..3-6.6.2.5 信号处理

6.6.2.6 键入fg指令,停止的前台进程重新开始运行。

6.6.2.7 键入kill指令与hello的pid17054, 进程被杀死。

图6.6.2.6-6.6.2.7 信号处理

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

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

6.7本章小结

shell是一个站在操作系统和应用程序之间的传递者。他代表用户执行应用程序,用它可以更方便的执行hello与管理hello。通过信号等方式,我们可以处理突发状况,实现一些功能,实现对进程的管理。内核对进程的调度使资源合理分配,互不干扰,提高了计算机系统的运行效率。

(第6章1分)

7 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:存储单元的地址可以用段基址(段地址)和段内偏移量(偏移地址)来表示,段基址确定它所在的段居于整个存储空间的位置,偏移量确定它在段内的位置,这种地址表示方式称为逻辑地址,通常表示为段地址:偏移地址的形式。在我们的课程中,逻辑地址是相对于当前进程数据段的地址,通常是一个段偏移值和段标识符的组合。

线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应的段基址生成了线性地址。

虚拟地址:在这里和线性地址的概念类似,虚拟内存(磁盘数据/主存数据的映射)中的每个字节都有一个虚拟地址,例如hello的反汇编文件中的起始地址0x401000就是一个虚拟地址,也是一个线性地址。

物理地址:物理存储器中的每一个字节都有唯一的存储器地址,称为物理地址,又叫绝对地址等。

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

逻辑地址=段选择符+偏移量,而每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。当我们已经获得了逻辑地址,就已经得到了段偏移量,然后根据段选择符前13位取出段寄存器对应的描述符,再据此得到基地址,最后将二者相加,于是得到了线性地址。

图7.2 逻辑地址到线性地址的变换

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

页表:页表数据结构用来管理虚拟地址。页表是一个是一个页表条目(PTE)的数组,虚拟地址空间中的每个页都有一个PTE。每个PTE由一个有效位和一个n位地址字段组成,有效位表明该虚拟页是否被缓存在主存中。如果设置了有效位,那么地址字段指向物理页的起始位置;如果没有设置有效位,空地址表示虚拟页未分配,否则这个地址指向该虚拟页映射到的磁盘数据的位置。

具体转换过程:虚拟地址由虚拟页号VPN与虚拟页偏移量VPO组成;物理地址由物理页号PPN和物理页偏移量PPO组成。MMU把VPN 对应到PTE(需要页表基址寄存器提供基址)。将页表条目中物理页号PPN(就是页表中的地址字段)与虚拟地址的VPO串联(注意是直接相连而不是相加)起来,就得到相应的物理地址。同时,也可以利用TLB缓存PTE加速地址的翻译。

图7.3 线性地址到物理地址的变换

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

7.4.1 TLB

在MMU中包括一个关于PTE的缓存,称为翻译后备缓冲器(TLB)。TLB是一个小的、虚拟寻址的缓存,每一行保存着一个由单个PTE组成的块,能够降低不命中带来的巨大时间开销。TLB通常有高的相连度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。

7.4.2 四级页表

VPN被解释成从低位到高位的4段,从高地址开始,第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;第二段VPN作为第二级页表的索引,用以确定第三级页表的基址;第三段VPN作为第三级页表的索引,用以确定第四级页表的基址;第四段VPN作为第四级页表的索引,若该位置的有效位为1,则该表项存储的是PPN。对于那些没有分配的虚拟地址,对应的多级页表根本不存在,只有当分配到它们时才会创建这些页表。

图7.4 四级页表

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

我们将物理地址分为缓存偏移CO、缓存组索引CI以及缓存标记CT。我们用组索引CI访问对应的组;然后利用标记CT来判断该级Cache中存储的数据是否有效。如果无效说明缓存不命中,需要继续从存储层次结构中的下一层中取出被请求的块,将新块存储在相应组的某个行中,可能会替换某个缓存行。

Intel Core i7使用了三级cache来加速物理内存访问,L1级cache作为L2级cache的缓存,L2级cache作为L3级cache的缓存,而L3级cache作为内存(DRAM)的缓存。我们的访问请求将依次沿存储器层次结构山下行,直至缓存命中。

图7.5.1 物理地址

图7.5.2 内存访问

7.6 hello进程fork时的内存映射

内核执行fork函数,为新进程创建各种数据结构,并分配给它一个唯一的PID。内核给新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本,将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制,这意味着进程需要写某区域时,这个写操作会触发一个保护故障,从而导致故障处理程序在物理内存中创建这个页面的一个新副本,而读操作不会建立新的副本。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,为每个进程保持了私有地址空间的抽象概念。同时延迟私有对象中的副本直到最后可能的时刻,充分利用物理内存。

7.7 hello进程execve时的内存映射

execve 函数在当前进程中加载并运行可执行目标文件 hello ,用 hello 程序替换父进程的副本。加载并运行 hello 需要一下几个步骤:

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

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

3.映射共享区域:如果 hello 程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内;

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

(和6.4内容重复了,就不再赘述了)

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

缺页故障:CPU把希望访问的虚拟地址给MMU,MMU通过VPN和页表基址获得了相应的PTE,当PTE的有效位未设置时,说明虚拟地址对应的内容还没有缓存在内存中,这时触发缺页故障,控制移交给内核。

缺页故障的处理:缺页异常导致控制转移到内核的缺页处理程序。处理程序首先判断虚拟地址是否合法。指令不合法(访问未分配页),缺页处理程序会触发一个段错误,从而终止这个进程。随后,判断内存访问是否合法。如果访问不合法(比如写了一个只读页面),缺页处理程序会触发一个保护异常,从而终止这个进程。对于正常的缺页,内核会选择牺牲页,如果这个牺牲页被修改过,就写会它的对应虚拟内存,然后它被驱逐出物理页,换入请求的页面并更新页表。处理程序返回当前的指令,这条指令就能正常执行了。

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

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存域,称为堆。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

显式分配器:要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符与c中的malloc和free相当。

显式分配器的实现:

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

  1. 显式空闲链表:显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如在每个空闲块中,都包含一个前驱与一个后继指针。

隐式分配器(垃圾收集器)可以自动地回收内存,但是C语言中它并不占主要地位,我们在此不作赘述。

图7.9 隐式空闲链表

7.10本章小结

本章内容是hello程序的存储管理,从逻辑地址、线性地址、虚拟地址和物理地址的关系出发,分析了地址的翻译;又引入了页表、Cache、内存映射的概念,并要求我们对fork、execve更加深入的理解;最后提及了动态内存管理的基本方法和策略。

(第7章 2分)

8 hello的IO管理

8.1 Linux的IO设备管理方法

首先,一个Linux文件就是一个m字节的序列。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输而出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix IO 接口,使得所有的输入和输出都能以一种统一且一致的方式来执行:

(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。

(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。

(3)改变当前的文件位置。

(4)读写文件。

(5)关闭文件。

Unix I/O标准函数:

1.open()函数

功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。

函数原型:int open(const char *pathname,int flags,int perms)

参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,

返回值:成功:返回文件描述符;失败:返回-1

2.close()函数

功能描述:用于关闭一个被打开的的文件

所需头文件: #include <unistd.h>

函数原型:int close(int fd)

参数:fd文件描述符

函数返回值:0成功,-1出错

3.read()函数

功能描述:

从文件读取数据。

所需头文件: #include <unistd.h>

函数原型:ssize_t read(int fd, void *buf, size_t count);

参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。

返回值:返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。

4.write()函数

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

所需头文件: #include <unistd.h>

函数原型:ssize_t write(int fd, void *buf, size_t count);

返回值:写入文件的字节数(成功)或-1(出错)

5.lseek()函数

功能描述:

用于在指定的文件描述符中将将文件指针定位到相应位置。

所需头文件:#include <unistd.h>,#include <sys/types.h>

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

参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负

返回值:成功:返回当前位移;失败:返回-1

8.3 printf的实现分析

printf函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息。它的源代码如下所示:

int printf(const char *fmt, ...)

{

int i;

char buf[256];

va_list arg = (va_list)((char*)(&fmt) + 4);

i = vsprintf(buf, fmt, arg);

write(buf, i);

return i;

}

printf执行过程如下:

  1. vsprintf 程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
  2. write函数接受待输出字符数组与需要输出的参数个数,执行写操作,把待输出字符数组中的i个元素输出。write函数执行系统调用sys_call。

3.sys_call将输出字符串中每个字符对应的ASCII码值复制到显存中。字符显示驱动子程序根据ASCII找到字模库相应的字形,并将每一个点的RGB颜色信息写入到显示vram,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar源代码:

int getchar(void)

{

static char buf[BUFSIZ];

static char* bb = buf;

static int n = 0;

if(n == 0)

{

n = read(0, buf, BUFSIZ);

bb = buf;

}

return(--n >= 0)?(unsigned char) *bb++ : EOF;

}

getchar能够读取stdin,然后获取输入的字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符输出到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

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

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

8.5本章小结

本章介绍了liunxI/O口的管理方法,介绍了相关的I/O接口及其函数,最后,深入分析了printf(),getchar()的实现。

(第8章1分)

结论

hello的一生:

源程序:人们在文本编辑器用高级语言写成hello.c源程序,标志着一个新生命的诞生。

预处理:预处理器宏替换、头文件文件包含、条件编译等,生成血肉更加丰满的hello.i。

编译:编译器进行词法分析,语法分析,把原程序变换成汇编指令,生成汇编文本文件hello.s,此时hello程序离能够自由运行的可执行文件的目标近了一大步。

汇编:汇编器将汇编指令翻译成机器语言,生成重定位信息,生成可重定位目标文件hello.o,hello程序从此离开人类的怀抱,成为专为机器而写的诗篇。

链接:链接器符号解析、重定位、动态链接,hello终于生成,它的生命即将跃入进程的洪流。

fork:shell会调用fork函数创建子进程,给hello一片舞台。

Execve:子进程调用execve函数,加载hello程序,进入hello的程序入口点,把hello推向舞台中央。

运行阶段:内核调度进程,进行异常处理。MMU、TLB、多级页表、cache、DRAM内存、动态内存分配器相互协作,管理内存。Unix I/O使得程序与文件进行交互。

终止:hello进程运行结束,shell回收hello,内核删除hello留下的所有信息,内存中没有多余的痕迹,而hello已经飞过。

计算机系统大作业对我们本学期的所学进行了一个整体的梳理,从编译到执行,从硬件到操作系统内核再到应用程序级软件,整个计算机系统显得如此精妙而动人。计算机是人类工业智慧的最高结晶,计算机系统是对这智慧的高度概括。读诗使人灵秀,读史使人明智,而计算机的科学既是人类信息文明的良史,也是人类智慧和知识的颂诗。

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

附件

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

hello.i:C预处理器产生的一个ASCII码的中间文件,用于分析预处理过程。

hello.s:C编译器产生的一个ASCII汇编语言文件,用于分析编译的过程。

hello.o:汇编器产生的可重定位目标程序,用于分析汇编的过程。

hello:链接器产生的可执行目标文件,用于分析链接的过程。

asm.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。

asm2.txt:hello的反汇编文件,用于分析可执行目标文件hello。

helloelf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。

helloelf2.txt:hello的ELF格式,用于分析可执行目标文件hello。

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

参考文献

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

[1]  (29条消息) 详解缺页中断-----缺页中断处理(内核、用户)_m0_37962600的博客-CSDN博客_缺页中断

[2]  (29条消息) malloc原理学习:隐式空闲链表_qqliyunpeng的博客-CSDN博客_隐式空闲链表

[3]  段页式访存——逻辑地址到线性地址的转换 - 简书 (jianshu.com)

[4]  RANDALE.BRYANT, DAVIDR.O‘HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2011.

[5]  虚拟内存_百度百科 (baidu.com)

[6] (29条消息) 浅析线性地址到物理地址的转换_weixin_30908649的博客-CSDN博客

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

2022哈工大计算机系统大作业——程序人生相关推荐

  1. 【2022】哈工大计算机系统大作业——程序人生Hello’s P2P

    2022哈工大计算机系统大作业--程序人生Hello's P2P 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概 ...

  2. 哈工大计算机系统大作业 程序人生-Hello’s P2P 2022

    2022哈工大计算机系统大作业 目录 摘 要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2在Ubun ...

  3. 哈工大计算机系统大作业 程序人生-Hello‘s P2P

    哈工大计算机系统大作业 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2 在Ubuntu下预处理的 ...

  4. 哈工大2022秋计算机系统大作业-程序人生(Hello‘s P2P)

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 学    号 班    级 学       生 指 导 教 师 刘宏伟 计算机科学与技术学院 ...

  5. 哈工大2022春计算机系统大作业:程序人生-Hello‘s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学   号 120L021305 班   级 2003002 学       生 李一凡 指 导 教 ...

  6. 哈工大 计算机系统大作业 程序人生-Hello’s P2P From Program to Process

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L020512 班    级 2003004 学       生 黄鹏程 指 导 ...

  7. 2022春 计算机系统大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 班 级 学 生 指 导 教 师 计算机科学与技术学院 2022年5月 摘 要 为深入理解计算机系统,本文以hel ...

  8. 哈工大计算机系统大作业-程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 学    号 2021110802 班    级 21w0312 学       生 黄键树 ...

  9. 哈工大计算机系统大作业 程序人生-Hello‘s P2P 020

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机科学与技术 学 号 2021112808 班 级 2103103 学 生 陶丽娜 指 导 教 师 刘宏伟 摘 要 本文详细介 ...

最新文章

  1. Office2013 分享
  2. centos进入单用户模式
  3. [云炬创业管理笔记]第二章成为创业者讨论3
  4. 查询复旦大学往年的考研成绩
  5. Taro+react开发(99):问答模块06实现加减
  6. java6_64.tar配置,Ubuntu 下Java-JDK6的安装与环境配置
  7. 史丰收速算|2014年蓝桥杯B组题解析第四题-fishers
  8. u盘资料误删怎么恢复 怎样找回u盘里误删的文件
  9. 第10节 文件共享服务器—创建/访问共享文件及禁用共享服务
  10. 电脑编程和计算机编程有什么区别,机器人编程与电脑编程有何区别?官方专家为你详细解说!...
  11. 子平格局——从旺格/从强格
  12. Error:403 No valid crumb was included in the request
  13. H5接入微信公众号方法(超详细)
  14. /etc/sysconfig/iptables.save文件的用途
  15. K-Means对红酒数据进行聚类||python
  16. 微信android字体颜色,如何用微信打出颜色各异的字
  17. centos6 安装完epel 解决yum的问题
  18. 使用Laravel View Composers在视图之间共享数据
  19. Android电话拨号器实例详解
  20. Oracle表空间(tablespaces)详解

热门文章

  1. MySQL-第七章-xtrabackup(XBK)工具使用
  2. 交叉编译 arm-poky-linux-gnueabi-gcc libmodbus库笔记
  3. 用C语言进行公英单位转换方法
  4. Python - 批量生成幻影坦克图片
  5. java SSH整合 SHIT
  6. Activiti 5 提示:Default sequenceflow has a condition, which is not allowed
  7. 童鞋们,我模拟了Google的电吉他,可录音,支持键盘
  8. 2023养老展,中福协养老展,中国国际养老服务业博览会
  9. Thread JUC
  10. iOS开发——网络请求案例汇总(AFNetworking)