CSAPP2021 helloP2P

计算机系统

大作业

题 目 *程序人生-Hello’s P2P *

专 业 *计算机科学与技术 *

学   号 *1190301804 *

班   级 *1936602 *

学 生 *梁成    *

指 导 教 师 *刘宏伟   *

计算机科学与技术学院

2021年6月

[[]{#_Toc250450163 .anchor}]{#_Toc225579639 .anchor}摘 要

hello’P2P介绍了hello.c程序从诞生到结束的全过程,包括预处理、编译、汇编、链接、进程等。我们主要使用gcc,edb,gdb等工具在linus下对hello进行一系列操作和分析,在这个过程中搭建起了认识计算机底层的框架,加深对CSAPP课本知识点的理解。

**关键词:**hello;P2P;计算机系统;Ubuntu ;CSAPP;

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

**
**

目 录

第1章 概述 - 5 -

1.1. Hello简介 - 5 -

1.2. 环境与工具 - 5 -

1.3. 中间结果 - 5 -

1.4. 本章小结 - 5 -

第2章 预处理 - 7 -

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

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

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

2.4. 本章小结 - 8 -

第3章 编译 - 9 -

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

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

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

3.3.1. 文件声明解析 - 10 -

3.3.2. 数据与赋值解析 - 10 -

3.3.3. 算术操作 - 11 -

3.3.4. 关系操作与转移控制 - 11 -

3.3.5. 数组/指针/结构操作 - 11 -

3.3.6. 函数操作 - 12 -

3.3.7. 类型转换 - 13 -

3.4. 本章小结 - 13 -

第4章 汇编 - 14 -

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

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

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

4.3.1. 获取ELF文件的命令 - 14 -

4.3.2. ELF文件结构: - 15 -

4.3.3. ELF Header - 15 -

4.3.4. Section Header - 16 -

4.3.5. 重定位节 - 16 -

4.3.6. 符号表 - 17 -

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

4.5. 本章小结 - 19 -

第5章 链接 - 21 -

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

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

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

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

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

5.5.1. hello相对hello.o的内容变化 - 25 -

5.5.2. 链接的过程 - 27 -

5.6. hello的执行流程 - 27 -

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

5.8. 本章小结 - 29 -

第6章 hello进程管理 - 30 -

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

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

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

6.4. Hello的execve过程 - 31 -

6.5. Hello的进程执行 - 31 -

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

6.7. 本章小结 - 35 -

第7章 hello的存储管理 - 36 -

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

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

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

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

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

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

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

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

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

7.10. 本章小结 - 45 -

第8章 hello的IO管理 - 46 -

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

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

8.3. printf的实现分析 - 47 -

8.4. getchar的实现分析 - 49 -

8.5. 本章小结 - 49 -

第9章 结论 - 50 -

第10章 附件 - 51 -

第11章 参考文献 - 52 -


概述

Hello简介

P2P过程:将hello.c经过 预处理->编译->汇编->链接
四个步骤生成hello的二进制可执行文件,在shell中为其fork出进程并执行。

020过程:shell开始执行并为其映射出虚拟内存,然后在开始运行进程的时候分配并载入物理内存,开始执行hello的程序,将output显示到屏幕,最后hello进程结束,shell回收内存空间。

环境与工具

硬件环境:处理器:Intel® Core™ i7-8550U CPU @ 1.80GHz 1.99GHz

RAM:8.00GB 系统类型:64 位操作系统,基于 x64 的处理器

软件环境:Windows10 64 位;Ubuntu 19.04

开发与调试工具:gcc,as,ld,vim,edb,readelf,VS

中间结果


文件的作用 文件名
*预处理后的文件 hello.i
*编译之后的汇编文件 hello.s
*汇编之后的可重定位目标文件 hello.o
*链接之后的可执行目标文件 Hello
*Hello.o 的 ELF 格式 elf.txt
*Hello.o 的反汇编代码 disassemble_hello.s
*hello的ELF 格式 helloELF.elf
hello 的反汇编代码 disassemble_hello_o.s


本章小结

本章主要介绍了hello的P2P,020过程,以及进行实验时的软硬件环境及开发与调试工具和在本论文中生成的中间结果文件。

(第1章0.5分)


预处理

预处理的概念与作用

  • 预处理概念:

预处理以展开的 # 开头,试图解释为预处理指令。其中 ISO
C/C++要求支持的包括#if、#ifdef、#ifndef、#else、#elif、
#endif(条件编译)、#define(宏定义)、
#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者
编译

  • 预处理作用:
  1. 将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c
    第 6-8 行中的#include<stdio.h>
    等命令告诉预处理器读取系统头文件 stdio.h unistd.h stdlib.h
    的内容,并把它直接插入到程序文本中。

  2. 特殊符号,预编译程序可以识别一些特殊的符号,
    预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

  3. 用实际值替换用#define 定义的字符串

  4. 根据#if 后面的条件决定需要编译的代码

在Ubuntu下预处理的命令

命令:cpp hello.c >hello.i

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3tz6vfao-1623927515230)(media/image2.png)]{width=“5.340972222222222in”
height=“0.2048611111111111in”}

图 1 cpp命令

Hello的预处理结果解析

预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。比如hello.c中第1行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并将它直接插入到程序文本中。

打开hello.i
,原来以#开头的行发生了扩展。总内容扩展到了3065行,而hello.c
的内容出现在3046行,在其之前的内容是#号对应宏展开(stdio.h , unistd.h ,
stdlib.h) 。

本章小结

本章主要介绍了预处理(包括头文件的展开、宏替换、去掉注释、条件编译)的概念和应用功能,以及Ubuntu下预处理的两个指令,同时具体到我们的hello.c文件的预处理结果hello.i文本文件解析,详细了解了预处理的内涵

(第2章0.5分)


编译

编译的概念与作用

  • 编译的概念:

编译阶段将文本文件hello.i
翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级程序机器语言指令。

编译包括以下基本流程:

  1. 语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。

  2. 中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。

  3. 代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。

  4. 目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。

  • 编译的作用:

它把高级语言翻译成更接近机器语言的汇编语言,使生成过程更加方便顺畅,以便机器读取。还具有语法检查,调试措施,修改手段。

在Ubuntu下编译的命令

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XskExuND-1623927515231)(media/image3.png)]{width=“5.280555555555556in” height=“0.25in”}

图 2 gcc命令

Hello的编译结果解析

文件声明解析


声明 含义
.file 源文件
.text 代码段
.data 数据段,存储已初始化的全局和静态c变量
.align 对齐格式
.type 符号类型
.size 数据空间大小
.section rodata 只读代码段
.global 全局变量
.string 字符串类型数据
.long 长整型数据


数据与赋值解析

全局变量


定义sleepsecs全局变量 在汇编文件中sleepsecs的内容

int sleepsecs=2.5; .text

                          .globl  sleepsecs
                      
                          .data
                      
                          .align 4
                      
                          .type   sleepsecs, @object
                      
                          .size   sleepsecs, 4

.globl声明了这是一个全局变量;.type说明了类型是一个数据;.size说明了这个变量的大小,这里sleepsecs变量占了4个字节

常量


printf打印字符串 字符串内容保存

printf(“Usage: Hello 学号 姓名!\n”); .LC0:

printf(“Hello %s %s\n”,argv[1],argv[2]); .string “Usage: Hello \345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201”

                                                  .LC1:
                                                  
                                                      .string "Hello %s %s\\n"

printf()函数的字符串常量被存储在.rodata节中

局部变量


初始化i=0 将0存入-4(%rbp)处

for(i=0;i<10;i++) .L2:

                         movl    \$0, -4(%rbp)
                     
                         jmp .L3

局部变量存储在寄存器或者栈中。

在这里局部变量i被存储在-4(%rbp)处。

算术操作


循环过程的自加操作 addl操作,栈上存储的变量+1

for(i=0;i<10;i++) addl $1, -4(%rbp)

                         cmpl    \$9, -4(%rbp)
                     
                     jle .L4

关系操作与转移控制


判断参数argc是否小于4 cmpl 操作

  if(argc!=3)             cmpl    \$3, -20(%rbp)
                      
                          je  .L2


循环过程中判断i是否<10 cmpl操作

for(i=0;i<10;i++) cmpl $9, -4(%rbp)

                              jle .L4

利用cmpl操作和je指令,实现转移控制

数组/指针/结构操作

字符指针数组 char *argv[]:存储用户输入的命令行信息地址

argv[0] 指向输入程序的路径与名称 , argv[1] argv[2]
指向字符串(学号/姓名)

该数组中每个元素大小为8bit,argv既是数组名也是数组的首地址

main 函数中访问数组元素argv[1],argv[2]时,按照起始地址 argv 大小 8B
计算数据地址取数据,在hello.s 中,使用两次(%rax)(两次 rax 分别为
argv[1]和 argv[2]的地址)取出其值。

如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bWM4ispI-1623927515233)(media/image4.png)]{width=“3.1590277777777778in”
height=“0.8715277777777778in”}

图 3 main函数的参数

函数操作

函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和可选的返回值实现某种功能。P
中调用函数 Q 包含以下动作:

1)传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q
的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q
后面那条指令的地址。

2)传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P
中返回一个值。

3)分配和释放内存:在开始时,Q
可能需要为局部变量分配空间,而在返回前,必须释放这些空间。

X86-64中,过程调用传递参数规则:

第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。

main()

传递控制:被系统启动函数调用。

传递数据:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。

返回内容:设置%eax为0并且返回,对应return 0

printf()


printf(“Usage: Hello 学号 姓名!\n”); leaq .LC0(%rip), %rdi

                                               call    puts@PLT

传递控制:if 判断满足条件后调用

传递数据:call puts时只传入了字符串参数的首地址;(%rdi)


printf(“Hello %s %s\n”,argv[1],argv[2]); .L4:

                                                      movq    -32(%rbp), %rax
                                                  
                                                      addq    \$16, %rax       
                                                  
                                                      **movq**    (%rax), %rdx    *//第三个参数:获得argv\[1\]的内容*
                                                  
                                                      movq    -32(%rbp), %rax
                                                  
                                                      addq    \$8, %rax        
                                                  
                                                      **movq**    (%rax), %rax    
                                                  
                                                      movq    %rax, %rsi      *//第二个参数:传入参数argv\[2\]*
                                                  
                                                      leaq    .**LC1**(%rip), %rdi*//第一个参数:.LC1的首地址*
                                                  
                                                      movl    \$0, %eax
                                                  
                                                      call    printf@PLT

传递控制:在for循环中调用

传递数据: call printf时还传入了argv[1]和argv[2]的地址。(%rsi %rdx)

exit()

传递控制:在if判断满足后调用

传递数据:传入参数为1(%edi)


exit(1); movl $1, %edi

                 call    exit@PLT

sleep()

传递控制:在循环内部用

传递数据:传入sleepsecs (%edi)


sleep(sleepsecs); movl %eax, %edi

                          call    sleep@PLT

getchar()


getchar(); call getchar@PLT


类型转换

程序中涉及隐式类型转换的是:

int sleepsecs=2.5;

当在 double 或 float 向 int
进行类型转换的时候,程序改变数值和位模式的原则是:值会向零舍入。例如
1.999 将被转换成 1,-1.999
将被转换成-1。进一步来讲,可能会产生值溢出的情况,与 Intel
兼容的微处理器指定位模式[10…000]为整数不确定值,一个浮点数到整数的转换,如果不能为该浮点数找到一个合适的整数近似值,就会产生一个整数不确定值。

浮点数默认类型为 double,所以上述强制转化是 double 强制转化为 int
类型。遵从向零舍入的原则,将 2.5 舍入为 2。

本章小结

本章主要介绍了编译的概念与作用,同时通过对hello.s汇编代码的分析,分析了C语言的各个数据类型与操作在汇编代码中的实现。

通过本章内容,我更深刻地理解了C语言与汇编语言之间的关系,也进一步熟悉了汇编代码,理解了编译的概念。


汇编

汇编的概念与作用

  • 概念

驱动程序运行汇编器as,将汇编语言(hello.s)翻译成机器语言(hello.o)的过程称为汇编。

(hello.o也是可重定位目标文件)

  • 作用

将高级语言转化为机器可直接识别执行的代码文件:将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序格式并保存在.o二进制文件中。

在Ubuntu下汇编的命令

Linus>as hello.s -o hello.o

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OZm7AQ4k-1623927515234)(media/image5.png)]{width=“4.310416666666667in”
height=“0.9923611111111111in”}

图 4 as命令获取hello.o

可重定位目标elf格式

获取ELF文件的命令

Linus> readelf -a hello.o >./elf.txt 获得ELF.txt文件

ELF文件结构:

如图,夹在 ELF
头和节头部表之间的都是节,ELF中存储了很多不同的节的信息,每一个节中保存了程序中对应的一些变量或者重定位等这些信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cCv4D1TI-1623927515235)(media/image6.png)]

图 5 ELF文件结构

ELF Header

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

ELF Header:

Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

Class: ELF64

Data: 2’s complement, little endian

Version: 1 (current)

OS/ABI: UNIX - System V

ABI Version: 0

Type: REL (Relocatable file)

Machine: Advanced Micro Devices X86-64

Version: 0x1

Entry point address: 0x0

Start of program headers: 0 (bytes into file)

Start of section headers: 1232 (bytes into file)

Flags: 0x0

Size of this header: 64 (bytes)

Size of program headers: 0 (bytes)

Number of program headers: 0

Size of section headers: 64 (bytes)

Number of section headers: 14

Section header string table index: 13

Section Header

节头部表描述不同节的位置和大小,目标文件中1每个节都有一个固定大小的条目,相关信息包括节的名称,类型,地址,偏移量,对齐,旗标等

Section Headers:

[Nr] Name Type Address Offset

   Size              EntSize          Flags  Link  Info  Align

[ 0] NULL 0000000000000000 00000000

   0000000000000000  0000000000000000           0     0     0

[ 1] .text PROGBITS 0000000000000000 00000040

   0000000000000085  0000000000000000  AX       0     0     1

[ 2] .rela.text RELA 0000000000000000 00000380

   00000000000000c0  0000000000000018   I      11     1     8

[ 3] .data PROGBITS 0000000000000000 000000c8

   0000000000000004  0000000000000000  WA       0     0     4

[ 4] .bss NOBITS 0000000000000000 000000cc

   0000000000000000  0000000000000000  WA       0     0     1

[ 5] .rodata PROGBITS 0000000000000000 000000cc

   000000000000002b  0000000000000000   A       0     0     1

[ 6] .comment PROGBITS 0000000000000000 000000f7

   000000000000002b  0000000000000001  MS       0     0     1

[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000122

   0000000000000000  0000000000000000           0     0     1

[ 8] .note.gnu.propert NOTE 0000000000000000 00000128

   0000000000000020  0000000000000000   A       0     0     8

[ 9] .eh_frame PROGBITS 0000000000000000 00000148

   0000000000000038  0000000000000000   A       0     0     8

[10] .rela.eh_frame RELA 0000000000000000 00000440

   0000000000000018  0000000000000018   I      11     9     8

[11] .symtab SYMTAB 0000000000000000 00000180

   00000000000001b0  0000000000000018          12    10     8

[12] .strtab STRTAB 0000000000000000 00000330

   000000000000004d  0000000000000000           0     0     1

[13] .shstrtab STRTAB 0000000000000000 00000458

   0000000000000074  0000000000000000           0     0     1

Key to Flags:

W (write), A (alloc), X (execute), M (merge), S (strings), I (info),

L (link order), O (extra OS processing required), G (group), T (TLS),

C (compressed), x (unknown), o (OS specific), E (exclude),

l (large), p (processor specific)

从节头部表中我们得知,hello.o一共有13个节。

重定位节

表述了各个段引用的外部符号等。在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型通过偏移量等信息计算出正确的地址。

Relocation section ‘.rela.text’ at offset 0x380 contains 8 entries:

Offset Info Type Sym. Value Sym. Name + Addend

00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4

000000000021 000d00000004 R_X86_64_PLT32 0000000000000000 puts - 4

00000000002b 000e00000004 R_X86_64_PLT32 0000000000000000 exit - 4

000000000054 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 1a

00000000005e 000f00000004 R_X86_64_PLT32 0000000000000000 printf - 4

000000000064 000a00000002 R_X86_64_PC32 0000000000000000 sleepsecs - 4

00000000006b 001000000004 R_X86_64_PLT32 0000000000000000 sleep - 4

00000000007a 001100000004 R_X86_64_PLT32 0000000000000000 getchar - 4

Relocation section ‘.rela.eh_frame’ at offset 0x440 contains 1 entry:

Offset Info Type Sym. Value Sym. Name + Addend

000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

以上8条重定位信息分别是对.L0(第一个printf 中的字符串)、puts 函数、exit
函数、.L1(第二个 printf 中的字符串)、printf 函数、sleepsecs、sleep
函数、getchar 函数进行重定位声明。


Offset 需要修改的引用的节偏移
Info 包括 symbol 和 type 两部分,其中 symbol 占前 4 个字节,type 占后 4 个字节,symbol 代 表重定位到的目标在.symtab中的偏移量,type 代表重定位的类型
Type 重定位到的目标的类型
Sys.value 重定向到的目标的名称
Sys.Name +addend 计算重定位位置的辅助信息,共占 8 个字节


符号表

.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。

Symbol table ‘.symtab’ contains 18 entries:

Num: Value Size Type Bind Vis Ndx Name

 0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 

 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c

 2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 

 3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 

 4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 

 5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 

 6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 

 7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 

 8: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 

 9: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 

10: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 sleepsecs

11: 0000000000000000   133 FUNC    GLOBAL DEFAULT    1 main

12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND \_GLOBAL\_OFFSET\_TABLE\_

13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND exit

15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

16: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND sleep

17: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND getchar

Hello.o的结果解析

执行objdump -d -r hello.o >disassemble_hello.s
,得到hello.o的反汇编如下:

hello.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:

0: f3 0f 1e fa endbr64

4: 55 push %rbp

5: 48 89 e5 mov %rsp,%rbp

8: 48 83 ec 20 sub $0x20,%rsp

c: 89 7d ec mov %edi,-0x14(%rbp)

f: 48 89 75 e0 mov %rsi,-0x20(%rbp)

13: 83 7d ec 03 cmpl $0x3,-0x14(%rbp)

17: 74 16 je 2f <main+0x2f>

19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>

     1c: R\_X86\_64\_PC32 .rodata-0x4

20: e8 00 00 00 00 callq 25 <main+0x25>

     21: R\_X86\_64\_PLT32   puts-0x4

25: bf 01 00 00 00 mov $0x1,%edi

2a: e8 00 00 00 00 callq 2f <main+0x2f>

     2b: R\_X86\_64\_PLT32   exit-0x4

2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)

36: eb 3b jmp 73 <main+0x73>

38: 48 8b 45 e0 mov -0x20(%rbp),%rax

3c: 48 83 c0 10 add $0x10,%rax

40: 48 8b 10 mov (%rax),%rdx

43: 48 8b 45 e0 mov -0x20(%rbp),%rax

47: 48 83 c0 08 add $0x8,%rax

4b: 48 8b 00 mov (%rax),%rax

4e: 48 89 c6 mov %rax,%rsi

51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58 <main+0x58>

     54: R\_X86\_64\_PC32 .rodata+0x1a

58: b8 00 00 00 00 mov $0x0,%eax

5d: e8 00 00 00 00 callq 62 <main+0x62>

     5e: R\_X86\_64\_PLT32   printf-0x4

62: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 68 <main+0x68>

     64: R\_X86\_64\_PC32 sleepsecs-0x4

68: 89 c7 mov %eax,%edi

6a: e8 00 00 00 00 callq 6f <main+0x6f>

     6b: R\_X86\_64\_PLT32   sleep-0x4

6f: 83 45 fc 01 addl $0x1,-0x4(%rbp)

73: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)

77: 7e bf jle 38 <main+0x38>

79: e8 00 00 00 00 callq 7e <main+0x7e>

     7a: R\_X86\_64\_PLT32   getchar-0x4

7e: b8 00 00 00 00 mov $0x0,%eax

83: c9 leaveq

84: c3 retq

与第三章的hello.s对照主要差别如下:

  • 分支转移函数

反汇编结果 hello.s
17: 74 16 je 2f <main+0x2f> je .L2


反汇编代码中可以看出相对偏移地址取代了hello.s中的标志位。反汇编代码跳转指令的操作数使用的不是段名称如.L2,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

  • 函数调用

反汇编结果 hello.s

6a: e8 00 00 00 00          callq  6f &lt;main+0x6f&gt;       call    sleep@PLT
                                                          
       6b: R\_X86\_64\_PLT32   sleep-0x4                  

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

这是因为 hello.c
中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其
call 指令后的相对地址设置为全
0(目标地址正是下一条指令),然后在.rela.text
节中为其添加重定位条目,等待静态链接的进一步确定。

  • 全局变量访问

反汇编结果

62: 8b 05 00 00 00 00       mov    0x0(%rip),%eax        \# 68 &lt;main+0x68&gt;

       64: R\_X86\_64\_PC32 sleepsecs-0x4

hello.s

  movq    -32(%rbp), %rax

  addq    \$8, %rax

在hello.s文件中对于全局变量的访问为LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip),原因与函数调用一样,全局变量的地址也是在运行时才确定,访问也需要经过重定位。

  • 进制表示

反汇编结果 hello.s

43: 48 8b 45 e0             mov    -0x20(%rbp),%rax       movq    -32(%rbp), %rax
                                                      
47: 48 83 c0 08             add    \$0x8,%rax             addq    \$8, %rax

反汇编后用16进制,hello.s用的是十进制。

本章小结

经过汇编过程后,hello.s被汇编器变为hello.o文件,此时hello.o已经是可以被机器读懂的二进制文件了。hello.o可重定位目标文件也为后面进行链接做好了准备。但是此时的hello仍然不能“上岗工作”,还需要进行最后一步链接才能变为可以被系统执行的可执行文件。

通过反汇编hello.o并与之前的hello.s进行比较,我进一步了解了汇编代码和机器代码之间的区别和联系。

(第4章1分)


链接

链接的概念与作用

  • 链接的概念

链接是将各种不同文件的代码和数据部分收集(符号解析和重定位)起来并组合成一个单一文件的过程。

  • 链接的作用

令源程序节省空间而未编入的常用函数文件(如printf.o)进行合并,生成可以正常工作的可执行文件。这令分离编译成为可能,节省了大量的工作空间。

在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的可执行目标文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3eyx8Dwz-1623927515236)(media/image7.png)]{width=“5.901388888888889in”
height=“1.6284722222222223in”}

图 6 链接得到hello的可执行文件

可执行目标文件hello的格式

使用命令: **readelf -a hello > helloELF.elf **

  • ELF header

ELF Header:

Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

Class: ELF64

Data: 2’s complement, little endian

Version: 1 (current)

OS/ABI: UNIX - System V

ABI Version: 0

Type: EXEC (Executable file)

Machine: Advanced Micro Devices X86-64

Version: 0x1

Entry point address: 0x4010d0

Start of program headers: 64 (bytes into file)

Start of section headers: 14200 (bytes into file)

Flags: 0x0

Size of this header: 64 (bytes)

Size of program headers: 56 (bytes)

Number of program headers: 12

Size of section headers: 64 (bytes)

Number of section headers: 27

Section header string table index: 26

  • Section Header

Section Header中包含了各节的名称,类型,地址,偏移量等信息。

可以看出此时节的数目由hello.o的14个增加到了27个,说明在链接过后有很多文件有添加进来。

Address:该节的虚拟地址(绝对地址)

Offset:该节在程序中地址的偏移量(相对地址)

Section Headers:

[Nr] Name Type Address Offset

   Size              EntSize          Flags  Link  Info  Align

[ 0] NULL 0000000000000000 00000000

   0000000000000000  0000000000000000           0     0     0

[ 1] .interp PROGBITS 00000000004002e0 000002e0

   000000000000001c  0000000000000000   A       0     0     1

[ 2] .note.gnu.propert NOTE 0000000000400300 00000300

   0000000000000020  0000000000000000   A       0     0     8

[ 3] .note.ABI-tag NOTE 0000000000400320 00000320

   0000000000000020  0000000000000000   A       0     0     4

[ 4] .hash HASH 0000000000400340 00000340

   0000000000000034  0000000000000004   A       6     0     8

[ 5] .gnu.hash GNU_HASH 0000000000400378 00000378

   000000000000001c  0000000000000000   A       6     0     8

[ 6] .dynsym DYNSYM 0000000000400398 00000398

   00000000000000c0  0000000000000018   A       7     1     8

[ 7] .dynstr STRTAB 0000000000400458 00000458

   0000000000000057  0000000000000000   A       0     0     1

[ 8] .gnu.version VERSYM 00000000004004b0 000004b0

   0000000000000010  0000000000000002   A       6     0     2

[ 9] .gnu.version_r VERNEED 00000000004004c0 000004c0

   0000000000000020  0000000000000000   A       7     1     8

[10] .rela.dyn RELA 00000000004004e0 000004e0

   0000000000000030  0000000000000018   A       6     0     8

[11] .rela.plt RELA 0000000000400510 00000510

   0000000000000078  0000000000000018  AI       6    21     8

[12] .init PROGBITS 0000000000401000 00001000

   000000000000001b  0000000000000000  AX       0     0     4

[13] .plt PROGBITS 0000000000401020 00001020

   0000000000000060  0000000000000010  AX       0     0     16

[14] .plt.sec PROGBITS 0000000000401080 00001080

   0000000000000050  0000000000000010  AX       0     0     16

[15] .text PROGBITS 00000000004010d0 000010d0

   0000000000000135  0000000000000000  AX       0     0     16

[16] .fini PROGBITS 0000000000401208 00001208

   000000000000000d  0000000000000000  AX       0     0     4

[17] .rodata PROGBITS 0000000000402000 00002000

   000000000000002f  0000000000000000   A       0     0     4

[18] .eh_frame PROGBITS 0000000000402030 00002030

   00000000000000fc  0000000000000000   A       0     0     8

[19] .dynamic DYNAMIC 0000000000403e50 00002e50

   00000000000001a0  0000000000000010  WA       7     0     8

[20] .got PROGBITS 0000000000403ff0 00002ff0

   0000000000000010  0000000000000008  WA       0     0     8

[21] .got.plt PROGBITS 0000000000404000 00003000

   0000000000000040  0000000000000008  WA       0     0     8

[22] .data PROGBITS 0000000000404040 00003040

   0000000000000008  0000000000000000  WA       0     0     4

[23] .comment PROGBITS 0000000000000000 00003048

   000000000000002a  0000000000000001  MS       0     0     1

[24] .symtab SYMTAB 0000000000000000 00003078

   00000000000004c8  0000000000000018          25    30     8

[25] .strtab STRTAB 0000000000000000 00003540

   0000000000000150  0000000000000000           0     0     1

[26] .shstrtab STRTAB 0000000000000000 00003690

   00000000000000e1  0000000000000000           0     0     1

Key to Flags:

W (write), A (alloc), X (execute), M (merge), S (strings), I (info),

L (link order), O (extra OS processing required), G (group), T (TLS),

C (compressed), x (unknown), o (OS specific), E (exclude),

l (large), p (processor specific)

hello的虚拟地址空间

使用edb加载hello, Data Dump 窗口可以查看加载到虚拟地址中的 hello 程序。

可以看出在 0x400000~0x401000
段中,程序被载入。这之间每个节的排列顺序与Section Headers中声明的顺序相同。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oZRd4VkE-1623927515237)(media/image8.png)]{width=“4.916666666666667in”
height=“1.992361111111111in”}

图 7 edb中节的内容

  • Program Headers

Program Headers:

Type Offset VirtAddr PhysAddr

             FileSiz            MemSiz              Flags  Align

PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040

             0x00000000000002a0 0x00000000000002a0  R      0x8

INTERP 0x00000000000002e0 0x00000000004002e0 0x00000000004002e0

             0x000000000000001c 0x000000000000001c  R      0x1

  \[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2\]

LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000

             0x0000000000000588 0x0000000000000588  R      0x1000

LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000

             0x0000000000000215 0x0000000000000215  R E    0x1000

LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000

             0x000000000000012c 0x000000000000012c  R      0x1000

LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

             0x00000000000001f8 0x00000000000001f8  RW     0x1000

DYNAMIC 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

             0x00000000000001a0 0x00000000000001a0  RW     0x8

NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300

             0x0000000000000020 0x0000000000000020  R      0x8

NOTE 0x0000000000000320 0x0000000000400320 0x0000000000400320

             0x0000000000000020 0x0000000000000020  R      0x4

GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300

             0x0000000000000020 0x0000000000000020  R      0x8

GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000

             0x0000000000000000 0x0000000000000000  RW     0x10

GNU_RELRO 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

             0x00000000000001b0 0x00000000000001b0  R      0x1

参数说明:

PHDR:保存程序头表

INTERP:动态链接器的路径

LOAD:可加载的程序段

DYNAMIN:保存了由动态链接器使用的信息

NOTE保存辅助信息

GNU_STACK:标志栈是否可执行

GNU_RELRO:指定重定位后需被设置成只读的内存区域

链接的重定位过程分析

使用 objdump -d -r hello 获得 hello 的反汇编代码。

hello相对hello.o的内容变化

与hello.o 反汇编文本相比,在 hello的反汇编文件中内容发生了变化。

  • 链接中新增了.plt.sec节 ,其中是一些函数如:puts、printf、getchar等

Disassembly of section .plt.sec:

0000000000401080 <puts@plt>:

401080: f3 0f 1e fa endbr64

401084: f2 ff 25 8d 2f 00 00 bnd jmpq *0x2f8d(%rip) # 404018 <puts@GLIBC_2.2.5>

40108b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

0000000000401090 <printf@plt>:

401090: f3 0f 1e fa endbr64

401094: f2 ff 25 85 2f 00 00 bnd jmpq *0x2f85(%rip) # 404020 <printf@GLIBC_2.2.5>

40109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

00000000004010a0 <getchar@plt>:

4010a0: f3 0f 1e fa endbr64

4010a4: f2 ff 25 7d 2f 00 00 bnd jmpq *0x2f7d(%rip) # 404028 <getchar@GLIBC_2.2.5>

4010ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

00000000004010b0 <exit@plt>:

4010b0: f3 0f 1e fa endbr64

4010b4: f2 ff 25 75 2f 00 00 bnd jmpq *0x2f75(%rip) # 404030 <exit@GLIBC_2.2.5>

4010bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

00000000004010c0 <sleep@plt>:

4010c0: f3 0f 1e fa endbr64

4010c4: f2 ff 25 6d 2f 00 00 bnd jmpq *0x2f6d(%rip) # 404038 <sleep@GLIBC_2.2.5>

4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

  • 链接中新增.init节 和 .plt节 和 .plt.sec节 和 .fini节

Disassembly of section .init:

Disassembly of section .plt:

Disassembly of section .plt.sec:

Disassembly of section .text:

Disassembly of section .fini:

  • 寻址方式变化

hello.o 中相对寻址:

19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>

hello 中直接寻址

40111e: 48 8d 3d df 0e 00 00 lea 0xedf(%rip),%rdi # 402004 <_IO_stdin_used+0x4>

这是因为hello.o
文件中对于某些地址定位暂时不明确,其地址也是在运行时确定的,因此访问需要重新定位。

  • 函数调用方式变化

hello.o 调用函数相对地址全设置为0

2a: e8 00 00 00 00 callq 2f <main+0x2f>

     2b: R\_X86\_64\_PLT32   exit-0x4

hello 调用函数有具体地址

40112f: e8 7c ff ff ff callq 4010b0 <exit@plt>

hello无需重定位所以没有hello.o中的重定位条目,跳转地址和函数调用地址在hello中都变成了虚拟内存地址。链接器将hello.o中的偏移量加上程序在虚拟内存中的起始地址0x0040000和.text节中的偏移量就得到了反汇编代码hello中的地址。

下面简要分析一下callq exit 的重定位PC相对引用

  1. 在ELF文件中能够找到exit对应的重定位条目如下

Offset Info Type Sym. Value Sym. Name + Addend

00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4

000000000021 000d00000004 R_X86_64_PLT32 0000000000000000 puts - 4

00000000002b 000e00000004 R_X86_64_PLT32 0000000000000000 exit - 4

可得:

r.offset=0x 2b

r.addend=0x -4

  1. <exit> 的首地址(Addr(s))

00000000004010b0 <exit@plt>:

  1. <main>的首地址(Addr(r.symbol))

0000000000401105 <main>:

由相对地址重定位计算公式

refaddr =Addr(s) + r.offset =0x4010b0+ 0x2b

*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr)

= (unsigned)[0x 4010b0 -0x 4 - (0x4010b0+0x2b)]

=(unsigned) [0x -84]

= 0x ff ff ff 7c

由于是小端序存储,所以callq的机器码为 e8 7c ff ff ff

链接的过程

链接的方式有静态和动态两种。最基本的链接叫做静态链接,就是将每个模块的源代码文件编译成目标文件(Linux:.o
Windows:.obj),然后将目标文件和库一起链接形成最后的可执行文件。

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。链接的过程如下:

  1. 地址和空间的分配

  2. 符号决议(用符号来标识一个地址)

  3. 重定向

hello的执行流程

用gdb调试 记录下执行过程中call命令进入的函数


程序名称 > 程序地址
ld-2.27.so!_dl_start > 0x7fce 8cc38ea0
ld-2.27.so!_dl_init > 0x7fce 8cc47630
hello!_start > 0x400500
libc-2.27.so! libc_start_main > 0x7fce 8c867ab0
-libc-2.27.so! cxa_atexit > 0x7fce 8c889430
-libc-2.27.so! libc_csu_init > 0x4005c0
hello!_init > 0x400488
libc-2.27.so!_setjmp > 0x7fce 8c884c10
-libc-2.27.so!_sigsetjmp > 0x7fce 8c884b70
–libc-2.27.so! sigjmp_save > 0x7fce 8c884bd0
hello!main > 0x400532
hello!puts@plt > 0x4004b0
hello!exit@plt > 0x4004e0
*hello!printf@plt > –
*hello!sleep@plt > –
*hello!getchar@plt > –
ld-2.27.so!_dl_runtime_resolve_xsave > 0x7fce 8cc4e680
-ld-2.27.so!_dl_fixup > 0x7fce 8cc46df0
–ld-2.27.so!_dl_lookup_symbol_x > 0x7fce 8cc420b0
libc-2.27.so!exit > 0x7fce 8c889128


Hello的动态链接分析

在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件hello。此时共享库中的代码和数据没有被合并到hello中。加载hello时,动态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。

动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本

在elf文件中可以找到:

[20] .got PROGBITS 0000000000403ff0 00002ff0

   0000000000000010  0000000000000008  WA       0     0     8

[21] .got.plt PROGBITS 0000000000404000 00003000

   0000000000000040  0000000000000008  WA       0     0     8

进入edb查看:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XsucLq1u-1623927515238)(media/image9.png)]{width=“5.492361111111111in”
height=“1.2951388888888888in”}

图 8 edb执行init前的地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mTfL3gvV-1623927515239)(media/image10.png)]{width=“5.477083333333334in”
height=“1.2652777777777777in”}

图 9 edb执行init后的地址

对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。

本章小结

在本章中主要介绍了链接的概念与作用、hello 的 ELF 格式,分析了 hello 的

虚拟地址空间、重定位过程、执行流程、动态链接过程。

(第5章1分)


hello进程管理

进程的概念与作用

  • 进程的概念:

进程是执行中程序的抽象。

  • 进程的作用:
  1. 每次运行程序时,shell创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。

  2. 进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。

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

  • Shell-bash的作用

Shell 是一个用 C 语言编写的程序,是用户使用 Linux
的桥梁,它提供了一个界面,用户通过这个界面访问操作系统内核的服务。

  • Shell-bash的处理流程

1)从终端读入输入的命令。

2)将输入字符串切分获得所有的参数

3)如果是内置命令则立即执行 ,否则调用相应的程序为其分配子进程并运行

shell 应该接受键盘输入信号,并对这些信号进行相应处理。

Hello的fork进程创建过程

在终端输入 ./hello 1190301804 liangcheng
运行的终端程序会对输入的命令行进行解析,因为 hello 不是一个内置的 shell
命令所以解析之后终端程序判断./hello
的语义为执行当前目录下的可执行目标文件 hello,之后终端程序首先会调用
fork
函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。

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

Hello的execve过程

exceve函数在当前进程的上下文中加载并运行一个新程序,execve函数加载并运行可执行目标文件hello,执行一次,并且从不返回。删除子进程现有的虚拟内存段,新的栈和堆段被初始化为零,新的代码和数据段被初始化为可执行文件中的内容。

Hello的进程执行

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

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

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这个决策就叫做调度,是由内核中称为调度器的代码处理的。

在内核调度了一个新的进程运行后,它就抢占当前进程,并使用上文所述的上下文切换的机制将控制转移到新的进程。内核代表的用户执行系统调用时,可能会发生上下文切换;中断也有可能引发上下文切换。

通过内核模式用户模式的切换描述用户态核心态转换的过程,在切换的第一部分中,内核代表进程
A 在内核模式下执行指令。然后在某一时刻,它开始代表进程
B(仍然是内核模式下) 执行指令。在切换之后,内核代表进程 B
在用户模式下执行指令。随后,进程 B
在用户模式下运行一会儿,直到磁盘发出一个中断信号,表示数据已经从磁盘传送到了内存。内核判定进程
B 已经运行了足够长的时间,就执行一个从进程 B 到进程 A
的上下文切换,将控制返回给进程 A 中紧随在系统调用 read
之后的那条指令。进程 A 继续运行,直到下一次异常发生,依此类推。

hello的异常与信号处理

异常类型:


类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回


处理方法:

  1. 中断

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Sy6ukDC-1623927515239)(media/image11.png)]{width=“3.6215277777777777in”
height=“1.1819444444444445in”}

图 10 中断的处理

  1. 陷阱

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZpLx6U5-1623927515240)(media/image12.png)]{width=“3.6819444444444445in”
height=“1.2270833333333333in”}

图 11 陷阱的处理

  1. 故障

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrYlt9kO-1623927515241)(media/image13.png)]{width=“3.848611111111111in”
height=“1.1895833333333334in”}

图 12 故障的处理

  1. 终止

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-emmalDtt-1623927515241)(media/image14.png)]{width=“3.598611111111111in”
height=“1.257638888888889in”}

图 13 终止的处理

执行过程中按键盘:

  1. 正常执行。如下图所示,为hello程序正常运行的结果,接着输入命令ps后执行,程序后台并没有hello进程正在执行了,说明进程正常结束,已经被回收了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xK7TKElI-1623927515242)(media/image15.png)]{width=“5.90625in” height=“2.1659722222222224in”}

图 14 正常执行

  1. 不停乱按:结果是程序运行情况和前面的相同,不同之处在于shell将我们刚刚乱输入的字符除了第一个回车按下之前的字符当做getchar的输入之外,其余都当做新的shell命令,在hello进程结束被回收之后,将会在命令行中尝试解释这些命令。中间没有任何对于进程产生影响的信号被产生。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fmei8dhM-1623927515242)(media/image16.png)]{width=“5.901388888888889in”
height=“4.007638888888889in”}

图 15 键盘乱按

  1. ctrl+c

在hello程序运行时输入CTRL+C会导致内核发送一个SIGINT信号到前台进程组的每个进程。默认情况下,结果是终止前台作业。

用ps和jobs观察,发现进程已经被终止。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3CkUDTuQ-1623927515243)(media/image17.png)]{width=“5.901388888888889in”
height=“1.0152777777777777in”}

图 16 ctrl+c

  1. ctrl+z

将会发送一个SIGTSTP信号给shell。然后shell将转发给当前执行的前台进程组,使hello进程挂起。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KXLOj8kD-1623927515243)(media/image18.png)]{width=“5.90625in” height=“0.5527777777777778in”}

图 17 ctrl+z

我们输入ps命令,hello进程仍然存在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XRkAvPrp-1623927515244)(media/image19.png)]{width=“5.552777777777778in”
height=“0.8104166666666667in”}

图 18 ctrl+z 用ps观察

输入jobs命令:hello已经停止,进程号为1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ku241pOu-1623927515244)(media/image20.png)]{width=“5.333333333333333in”
height=“0.3861111111111111in”}

图 19 ctrl +z 用jobs观察

使用fg 1(1是进程号)命令将其调回前台,继续执行:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pBpkJHre-1623927515245)(media/image21.png)]{width=“5.409027777777778in”
height=“1.6972222222222222in”}

图 20 ctrl+z 后 用fg 调回

kill命令: kill后无法用ps看到进程号,jobs也为空。进程已经被杀死。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EvMcYhyr-1623927515245)(media/image22.png)]{width=“5.90625in” height=“0.9083333333333333in”}

图 21 kill指令

本章小结

本章了解了hello进程的执行过程。主要讲hello的创建、加载和终止,通过键盘输入。程序是指令、数据及其组织形式的描述,进程是程序的实体。可以说,进程是运行的程序。在hello运行过程中,内核有选择对其进行管理,决定何时进行上下文切换。也同样是在hello的运行过程中,当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对不同的异常信号,hello也有不同的处理结果。

(第6章1分)


hello的存储管理

hello的存储器地址空间

  • 逻辑地址

逻辑地址(Logical
Address)是指由程序hello产生的与段相关的偏移地址部分(hello.o)。

  • 线性地址

线性地址(Linear
Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。

  • 虚拟地址

有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。

  • 物理地址

物理地址(Physical
Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。

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

一个逻辑地址由两部份组成,段标识符、段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygFgYtKM-1623927515245)(media/image23.png)]

图 22 段选择符

索引号,是“段描述符(segment
descriptor)”,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成,如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lms1Zy40-1623927515246)(media/image24.png)]

图 23 段描述符

Base字段:它描述了一个段的开始位置的线性地址。
Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。

当段选择符中的T1字段=0,表示用GDT;若为1,表示用LDT。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。再看图7-3比起来要直观些:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEFQLTnV-1623927515246)(media/image25.png)]

图 24 逻辑地址转线性地址

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],

1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,Base(基地址)就知道了。

3、把Base + offset,就是要转换的线性地址了。

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

页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page
frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个段的任务结构即图中的task_struct,其中条目mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,一个链表条目对应一个段,所以链表相连指出了hello进程虚拟内存中的所有段。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3iltck0-1623927515247)(media/image26.png)]{width=“4.280555555555556in”
height=“2.6284722222222223in”}

图 25 段集合示意图

而物理内存被划分为一小块一小块,每块被称为帧(Frame)。分配内存时,帧是分配时的最小单位,最少也要给一帧。

在虚拟内存中,与帧对应的概念就是页(Page)。虚拟地址的表示如下,由虚拟页号VPN与虚拟页偏移量VPO组成。

MMU利用虚拟页号(VPN)来在虚拟页表中选择合适的PTE,当找到合适的PTE之后,PTE中的物理页号(PPN)和虚拟页偏移量(VPO)就会组合形成物理地址。其中VPO与PPO相同,因为虚拟页大小和物理页大小相同,所需要的偏移量位数也就相同。此时,物理地址就通过物理页号先找到对应的物理页,然后再根据物理页偏移找到具体的字节:

1.如果有效位是 0+NULL 则代表没有在虚拟内存空间中分配该内存;

2.如果是有效位 0+非
NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中;

3.如果有效位是 1 则代表该内存已经缓存在了物理内存中,可以得到其物理页号
PPN,与虚拟页偏移量共同构成物理地址 PA。

如下图所示,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgDd1sEf-1623927515248)(media/image27.png)]{width=“3.265277777777778in”
height=“1.6895833333333334in”}

图 26 页式管理示意图

页式管理优缺点分析:

优点:

1、由于它不要求作业或进程的程序段和数据在内存中连续存放,从而有效地解决了碎片问题。

2、动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。

缺点:

1、要求有相应的硬件支持。例如地址变换机构,缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本。

2、增加了系统开销,例如缺页中断处理机,

3、请求调页的算法如选择不当,有可能产生抖动现象。

4、虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到利用果页面较大,则这一部分的损失仍然较大。

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

VA由VPN和VPO组成。PA由PPN与PPO组成。

VPN可以作为在TLB中的索引,如下图所示,TLB可以看作是一个PTE的cache,将常用的PTE缓存到TLB中,加速虚拟地址的翻译。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o2V1iSP9-1623927515248)(media/image28.png)]{width=“3.9923611111111112in”
height=“2.848611111111111in”}

图 27 core-i7地址翻译情况

  1. 如果能够在TLB中找到与VPN对应的PTE,即为TLB
    命中,TLB直接给出PPN,然后PPO即为VPO,这样就得到了物理地址PA。

  2. 如果TLB没有命中,那么就需要到四级页表中寻址:

将虚拟地址的VPN划分为相等大小的四部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N7ZWECIe-1623927515249)(media/image29.png)]{width=“3.901388888888889in”
height=“2.3784722222222223in”}

图 28 core-i7 页表翻译

然后解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复4次,在第4级页表获得了页表条目,将PPN与VPO组合获得PA。

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

MMU获得PA后,根据cache大小将PA分为(CT,CI,CO)三部分。

然后根据CI(倒数7-12位)进行组索引,每组8路。
对8路的块分别匹配CT(前40位)。

  1. 如果匹配成功且块内valid位为1,则命中。命中后根据CO(后六位)取出数据并返回。

  2. 如果不命中,则需要向下一级缓存中查询数据(L2 cache–>L3 cache
    –>内存), 查询到数据后,用相应的放置策略更新各级cache。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPJz7rYO-1623927515249)(media/image30.png)]{width=“3.8027777777777776in”
height=“2.6215277777777777in”}

图 29 三级cache下物理内存访问

hello进程fork时的内存映射

理解了虚拟内存和内存映射,那么我们就可以清晰地知道fork函数是如何创建一个带有自己独立虚拟地址空间的新进程的。

当fork
函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID
。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct
、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork
在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork
时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

hello进程execve时的内存映射

在bash中的进程中执行了如下的execve调用:execve(“hello”,NULL,NULL);

execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。

下面是加载并运行hello的几个步骤:

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

  2. 映射私有区域

  3. 映射共享区域

  4. 设置程序计数器(PC)

  5. exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

下一次调度这个进程时,它将从这个入口点开始执行。Linux
将根据需要换入代码和数据页面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KHe5ZOBT-1623927515250)(media/image31.png)]{width=“3.2729166666666667in”
height=“2.454861111111111in”}

图 30 加载器映射用户地址区域

缺页故障与缺页中断处理

在虚拟内存的习惯说法中,DRAM 缓存不命中称为缺页(page fault)
。页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的。

假设 MMU 在试图翻译某个虚拟地址 A
时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:

  1. 先确认是不是一个合法的地址:即通过不断将这个地址与每个区域的vm_start&vm_end进行比对,如果并不是在一个区域里的话,就给出segmentation
    fault,因为它引用了一个不合法的地址

  2. 确认访问权限是不是正确的:即如果这一页是只读页,但是却要做出写这个动作,那明显是不行的。如果做出了这类动作,那么处理程序就会触发一个保护异常,默认行为是结束这个进程

  3. 当确认了是合法地址并且是符合权限的访问,那么就用某个特定算法选出一个牺牲页,如果该页被修改了,就将此页滑出(swap
    out)并且swap
    in那个被访问的页,并将控制传递到触发缺页中断的那条指令,这条指令继续执行的时候就不会触发缺页中断,这样就可以继续执行下去。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n4xHoz8S-1623927515251)(media/image32.png)]{width=“4.038194444444445in”
height=“2.401388888888889in”}

图 31 linus缺页处理

动态存储分配管理

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

动态内存分配主要有两种基本方法与策略:

  • 带边界标签的隐式空闲链表分配器管理

带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JCJUY07h-1623927515252)(media/image33.png)]{width=“4.424305555555556in”
height=“1.9618055555555556in”}

图 32 隐式空间链表的堆块示意图

隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。

当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。

  • 显式空间链表管理

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VBhOSSko-1623927515253)(media/image34.png)]{width=“4.727083333333334in”
height=“2.3784722222222223in”}

图 33 显式空闲表的堆块示意图

显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。

本章小结

本章主要介绍了 hello 的存储器地址空间、 intel 的段式管理、 hello
的页式管理,在指定环境下介绍了 VA 到 PA 的变换、物理内存访问,还介绍
hello 进程 fork 时的内存映射、 execve
时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7章 2分)


hello的IO管理

Linux的IO设备管理方法

  • 设备的模型化:所有的IO设备都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。

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

简述Unix IO接口及其函数

Unix I/O 接口统一操作:

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

  2. Shell
    创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

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

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

  5. 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O 函数:

  1. open()函数

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

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

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

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

  1. close()函数

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

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

函数原型:int close(int fd)

参数:fd文件描述符

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

  1. read()函数

功能描述: 从文件读取数据。

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

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

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

返回值:返回所读取的字节数;0(读到EOF);-1(出错)。

  1. write()函数

功能描述: 向文件写入数据。

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

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

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

  1. lseek()函数

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

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

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

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

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

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函数内容:

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

将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度i。

  1. vsprintf函数

int vsprintf(char *buf, const char *fmt, va_list args)

{

char* p;

char tmp[256];

va_list p_next_arg = args;

for (p=buf;*fmt;fmt++) {

if (*fmt != ‘%’) {

*p++ = *fmt;

continue;

}

fmt++;

switch (*fmt) {

case ‘x’:

itoa(tmp, *((int*)p_next_arg));

strcpy(p, tmp);

p_next_arg += 4;

p += strlen(tmp);

break;

case ‘s’:

break;

default:

break;

}

}

return (p - buf);

}

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

write(buf, i);

用write函数将buf中的i个元素写到终端。

  1. write函数

write:

mov eax,\_NR\_write

mov ebx,\[esp+4\]

mov ecx,\[esp+8\]

int INT_VECTOR_SYS_CALL

在write函数中,ecx:字符个数,ebx:第一个字符的地址,最后调用syscall。

  1. syscall

实现较为复杂。功能:不断打印出字符,直到遇到‘\0’停止。

printf实现流程如下:

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

(2):vsprintf的输出到write系统函数中。在Linux下,write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。

(3):显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。

getchar的实现分析

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。

如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

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

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

本章小结

本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O
接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程。

(第8章1分)

结论

hello程序终于完成了它艰辛的一生,它的一生有这么几件大事:

  1. hello.c经过预编译,拓展得到hello.i文本文件

  2. hello.i经过编译,得到汇编代码hello.s汇编文件

  3. hello.s经过汇编,得到二进制可重定位目标文件hello.o

  4. hello.o经过链接,生成了可执行文件hello

  5. bash进程调用fork函数,生成子进程;

  6. hello由execve函数加载运行当前进程的上下文中加载并运行新程序

  7. hello通过解析得到物理地址PA。

  8. hello再运行时会调用一些函数,比如printf函数,这些函数与linux
    I/O的设备模拟化密切相关。

  9. hello最终被shell父进程回收,内核会收回为其创建的所有信息

个人感悟:CSAPP不愧是本科期间收获最大的一门课,大作业也设计得如此精巧!它带我们领略的计算机内部的美丽风景,从浅入深地剖析讲解了计算机的方方面面,领我们从顶层到达了底层!体验完了hello一生,我也对计算机系统有了更深入的了解!

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


附件

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


文件的作用 文件名
*预处理后的文件 hello.i
*编译之后的汇编文件 hello.s
*汇编之后的可重定位目标文件 hello.o
*链接之后的可执行目标文件 Hello
*Hello.o 的 ELF 格式 elf.txt
*Hello.o 的反汇编代码 disassemble_hello.s
*hello的ELF 格式 helloELF.elf
hello 的反汇编代码 disassemble_hello_o.s


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


参考文献

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

  1. RandalE.Bryant, DavidO’Hallaron, 龚奕利,等. 深入理解计算机系统[J].
    中国电力出版社, 2004.

  2. 刘兵, 吴煜煌. Linux 实用教程[J]. 中国水利水电出版社, 2004. C.
    Linus操作基础[M/OL]. CSDN, 2018-05-27.

  3. 李洛, 黄达峰. Linux教程[M]. 清华大学出版社, 2005.

  4. ELF
    构造:https://www.cs.stevens.edu/~jschauma/631/elf.html

  5. 进程的睡眠、挂起和阻塞:https://www.zhihu.com/question/42962803

  6. printf
    函数剖析https://blog.csdn.net/zhengqijun_/article/details/72454714

  7. 虚拟地址
    https://baike.baidu.com/item/虚拟地址/1329947?fr=aladdin

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

上一篇:程序人生-Hello’s P2P


下一篇:【BigData】Java基础_DataOutputStream与DataInputStream