哈工大2022秋计算机系统大作业——程序人生

摘  要

本文对hello程序的整个生命周期进行了系统的分析,分析hello程序的过程也是对计算机系统的一次漫游。Hello程序在生命周期中经历了From Program to process 和From Zero to Zero的过程。Hello.c 通过预处理(cpp)生成hello.i,经过C编译器(ccl)生成汇编文件hello.s,然后通过汇编器(as)生成可重定位文件hello.o,最后通过链接器(ld)和库中的可重定位目标文件链接,生成可执行目标文件hello。Shell接受到./hello的指令后调用fork函数创建进程,execve加载hello进入内存,CPU控制程序逻辑流的运行、中断、上下文切换和异常处理,最后结束进程并由父进程进行回收,hello的生命结束。

关键词:预处理;编译;汇编;链接;进程;存储;IO管理                           

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

目  录

第1章 概述................................................................................... - 4 -

1.1 Hello简介............................................................................ - 4 -

1.2 环境与工具........................................................................... - 4 -

1.3 中间结果............................................................................... - 4 -

1.4 本章小结............................................................................... - 4 -

第2章 预处理............................................................................... - 5 -

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

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

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

2.4 本章小结............................................................................... - 5 -

第3章 编译................................................................................... - 6 -

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

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

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

3.4 本章小结............................................................................... - 6 -

第4章 汇编................................................................................... - 7 -

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

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

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

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

4.5 本章小结............................................................................... - 7 -

第5章 链接................................................................................... - 8 -

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

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

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

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

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

5.6 hello的执行流程................................................................. - 8 -

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

5.8 本章小结............................................................................... - 9 -

第6章 hello进程管理.......................................................... - 10 -

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

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

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

6.4 Hello的execve过程........................................................ - 10 -

6.5 Hello的进程执行.............................................................. - 10 -

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

6.7本章小结.............................................................................. - 10 -

第7章 hello的存储管理...................................................... - 11 -

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

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

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

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

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

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

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

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

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

7.10本章小结............................................................................ - 12 -

第8章 hello的IO管理....................................................... - 13 -

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

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

8.3 printf的实现分析.............................................................. - 13 -

8.4 getchar的实现分析.......................................................... - 13 -

8.5本章小结.............................................................................. - 13 -

结论............................................................................................... - 14 -

附件............................................................................................... - 15 -

参考文献....................................................................................... - 16 -

第1章 概述

1.1 Hello简介

1.1.1 P2P:From Program to Process

在Linux系统上,从程序到进程需要四个阶段。

阶段1:hello.c经过C预处理cpp进行预处理生成hello.i

阶段2:hello.i通过C编译器(ccl)得到汇编语言文件hello.s

阶段3:运行汇编器(as)将hello.s翻译成可重定位目标文件hello.o

阶段4:链接器(ld)将hello.o和系统目标文件如printf.o组合起来,生成可执行目标文件hello

可执行目标文件通过shell命令./hello开始了属于它的进程。

图1:编译系统

1.1.2 020:From Zero to Zero

程序从键盘输入到计算机中的时刻起才开始存在,在经过从程序到进程的四个阶段后,通过shell命令./hello,shell通过fork函数创建了新的进程,之后调用execve加载并运行可执行目标文件hello,操作系统为其分配虚拟内存空间,在物理内存和虚拟内存之间建立映射。执行过程中, 虚拟内存为进程提供独立的空间,数据从磁盘传输到CPU中,TLB、分级页表等保障了数据的高效访问,I/O管理与信号处理共同实现了hello的输入输出。

进程运行结束后,shell父进程回收hello进程,对应的虚拟空间以及相关数据结构被释放,hello便经历了从无到有再到无的过程。

1.2 环境与工具

硬件环境:AMDR7处理器 64位  1.9GHz 16GRAM

软件环境:Windows11家庭中文版  Vmware16.2.4  Ubuntu20.04

工具:        Vim+gcc,gdb,objdump,Hexedit

1.3 中间结果

hello.c :源程序

hello.i :hello.c预处理后的源程序文件

hello.s :hello.i编译后得到的汇编程序

hello.o :hello.s汇编后得到的可重定位目标文件

hello :可执行目标文件

hello_o_elf :hello.o的elf文件

hello_elf :hello的elf文件

hello_o_asm :hello.o的反汇编文件

hello_asm :hello的反汇编文件

1.4 本章小结

本章简单介绍了hello的从程序到进程,从无到有的过程,给出了进行实验的软硬件环境以及实验过程中生成的文件。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

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

预处理的作用:

1.将头文件的内容插入程序中,比如把stdio.h中的内容插入到程序中

2.进行宏替换,用实际值替换用#define定义的字符串

3.删除注释

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

2.2在Ubuntu下预处理的命令

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

图2  Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

通过上述的指令得到了hello.i文本文件。通过gedit打开hello.i文件,我们可以看见经过预处理,文本文件已经扩展到了3060行。

Hello.c的源代码在3037行开始,前面的是插入的库文件的内容,比如stdio.h, unistd.h, stdlib.h。以stdio.h为例,我们到/usr/include/的目录下打开stdio.h文件,可以看到这个文件中也有大量的#define,预处理器对其进行递归展开,最终的hello.i中是没有#define的。stdio.h中声明了许多标准函数,其中包括常用的printf和scanf。

此外预处理器cpp将hello.c中的注释删除了。

图3:hello.i的源代码部分

图4:标准库stdio.h中的部分内容

图5:hello.i的部分函数声明

2.4 本章小结

本章介绍了程序预处理的概念、作用,在Ubuntu操作系统中进行了实操演示,并且通过和hello.c程序进行比较,分析了预处理得到的hello.i中的内容,进一步展示了预处理的作用。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译程序的工作是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译为等价的汇编代码。编译器(ccl)将hello.i文件翻译成hello.s文件。

编译的作用:

编译通过5个阶段,将高级语言程序转化为计算机能解读、运行的低级语言。这5个步骤分别是——词法分析、语法分析、语义检查和中间代码生成、代码优化、目标代码生成。

1.词法分析:对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。

2.语法分析:以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。

3. 语义检查和中间代码生成:由语义分析器完成,指示判断是否合法,并不判断对错。中间代码的作用是使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。

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

5.目标代码生成:把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,需经过汇编程序汇编后,称为可执行的机器语言代码。

3.2 在Ubuntu下编译的命令

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

 图6:Ubuntu下的编译命令

3.3 Hello的编译结果解析

3.3.1数据

(1)数字常量:

在if语句 if(argc!=4)中的数字常量4保存在.text段中,作为指令的一部分。

图7:hello.s中数据常量4作为指令的一部分

同理for循环中的0,9,1,2,3也被存储在.text段中。

(2)字符串常量:

在printf和scanf中出现的字符串常量被存储在rodata节中。

例如:printf("用法: Hello 学号 姓名 秒数!\n");     这条语句中的字符串保存在rodata节部分

图8:hello.s中printf中的字符串常量

(3)局部变量

局部变量保存在栈中或是寄存器中。比如循环中的局部变量i,在循环开始的时候给i赋值为0。i被存储在栈上,地址为%rbp – 4。

图9:hello.s中循环变量i的初始化

(4)全局变量

全局变量被存储在.data节或是.bss节。已经赋初值的全局变量存储在.data节中,为赋值或是初值赋为0的全局变量在.bss节中。

3.3.2 算数操作

在循环中,循环变量i在每次循环结束后会进行i++的自增操作,在机器指令中这是通过对add指令对栈中的变量i进行加1操作,指令如下:

addl      $1, -4(%rbp)

3.3.3关系操作和控制转移

(1)相等判断

C程序中第13行判断命令行参数个数是否是4个,如果不是,则打印命令行输入的正确格式。

汇编代码是通过cmpl来是实现的,cmpl s1 s2指令是判断s2-s1,只修改条件码,不修改操作数。Je基于操作码,判断是否进行跳转,如果操作码的ZF为1,表示两数相等,则进行跳转。

图10:hello.s中的相等判断和控制转移

(2)小于判断

类似的,在for循环中需要判断循环变量i是否满足循环继续的条件。For循环中的小于9,转换为和常数8进行比较,若小于等于8则跳转到循环的操作部分。

图11:for循环中循环中止条件的判断

3.3.4数组/指针操作

下面的汇编指令将主函数读取的命令行参数argc,argv从寄存器中转移到栈中。

其中argv是一个指针数组的首地址,可以在这个基址上加特定的值,对数组的内容进行访问。

图12:hello.s中命令行参数的读取

在调用printf函数之前,汇编代码将需要打印的内容移到相应的寄存器中,%rbp-32+16 = %rbp-16存储的是argv[2],即指向姓名字符串的指针,作为printf的第三个参数,放在rdx中。同理,%rbx-8存储的是argv[1],存放的是指向学号字符串的指针,作为printf的第二个参数,放在%rsi中。

图13:hello.s中基于基址的数组操作

3.3.5 函数操作

X86-64位机中,函数的前6个参数通过寄存器传递,后面的参数通过栈来进行传递。6个寄存器分别是rdi,rsi,rdx,rcx,r8,r9。

1main函数:

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

函数调用:被系统启动函数调用。

函数返回:设置%eax为0并且返回,对应return 0 。

源代码:

int main(int argc, char *argv[])

汇编代码:

main:

.LFB6:

    .cfi_startproc

    pushq   %rbp

    .cfi_def_cfa_offset 16

    .cfi_offset 6, -16

    movq    %rsp, %rbp

    .cfi_def_cfa_register 6

    subq    $32, %rsp

    movl    %edi, -20(%rbp)

    movq    %rsi, -32(%rbp)

可见argc存储在%edi中,argv存储在%rsi中;

(2)printf函数

参数传递:call puts时只传入了字符串参数首地址;for循环中call  printf时传入了 argv[1]和argc[2]的地址。

函数调用:if判断满足条件后调用,与for循环中被调用。

源代码1:

printf("用法: Hello 学号 姓名 秒数!\n");

汇编代码1:

.LFB6:

    cmpl    $4, -20(%rbp)

    je  .L2

    leaq    .LC0(%rip), %rdi

    call    puts@PLT

源代码2:

    printf("Hello %s %s\n", argv[1], argv[2]);

汇编代码2:

.L4:

    movq    -32(%rbp), %rax

    addq    $16, %rax

    movq    (%rax), %rdx

    movq    -32(%rbp), %rax

    addq    $8, %rax

    movq    (%rax), %rax

    movq    %rax, %rsi

    leaq    .LC1(%rip), %rdi

    movl    $0, %eax

    call    printf@PLT

(3)exit函数:

参数传递:传入的参数为1,再执行退出命令

函数调用:if判断条件满足后被调用.

源代码:

    exit(1);

汇编代码:

.LFB6:

    movl    $1, %edi

    call    exit@PLT

(4)sleep函数:

参数传递:传入参数atoi(argv[3]),

函数调用:for循环下被调用,call sleep

源代码:

    sleep(atoi(argv[3]));

汇编代码:

.L4:

    movq    -32(%rbp), %rax

    addq    $24, %rax

    movq    (%rax), %rax

    movq    %rax, %rdi

    call    atoi@PLT

    movl    %eax, %edi

    call    sleep@PLT

(5)getchar函数:

函数调用:在main中被调用,汇编指令为call getchar

源代码:

    getchar();

汇编代码:

.L3:

    call    getchar@PLT

3.4 本章小结

本章首先介绍了编译的概念、步骤以及进行编译的命令行。然后结合hello.c和hello.s文件分析了编译器是如何把高级语言转化为汇编指令的。

在3.3.1中我们分析了数据的不同的类型,以及它们的保存位置。常数保存在.text中,字符串保存在rodata中,数据的赋值可以使用move指令完成。3.3.2节中介绍了算数操作,比如add,sub等,编译器能够对数据进行一定的运算。3.3.3节中介绍了控制转移,包括条件判断、for循环,通过标记为进行一定的跳转实现程序逻辑的实现。3.3.4中介绍了数组和指针,对于数组的方位可以通过基址加上偏移量的方式进行访问。3.3.5中介绍了hello.c中的不同函数,函数的参数可以通过寄存器和栈进行传递。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念

驱动程序运行汇编器as,将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编。这个机器语言文件是可重定位目标文件(二进制)。

作用

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

4.2 在Ubuntu下汇编的命令

汇编指令:as hello.s -o hello.o

图14:汇编指令

4.3 可重定位目标elf格式

4.3.1 输出elf文件的指令

输出hello_o_elf.txt的指令:readelf -a hello.o > ./hello_o_elf.txt

图15:输出elf文件的指令

4.3.2 ELF

包含了魔数、文件类别,数据表示方式,elf头大小,节的大小和数量、节头表在字符表中的下标。Elf头内容如下:

图16:hello_o_elf.txt的elf头部分

4.3.2节头表

节头表中描述了hello.o中各个节的信息,包括节的大小,读写属性,偏移量等信息。通过节头表能够找到相应的节,比如.text节,.data,.bss节等。

节头表的信息如下:

 图17:hello_o_elf.txt中的节头表

4.3.3 重定位节

因为hello.o中存在对外部符号的引用,在hello.o中没有定义,所有需要重定位节把这些符号的位置以及寻址方式(绝对寻址还是PC相对寻址)标记出来,这样在连接的时候可以根据重定位节,对空缺的符号进行填充。

重定位节的内容如下:

图18:hello_o_elf.txt中的重定位节

4.3.4 符号表

.symtab符号中,记录的是程序中定义或者引用的函数、全局变量、常量的信息。比如main,puts,exit等函数,1,3,4等数据常量。

图19:hello_o_elf.txt中的符号表

4.4 Hello.o的结果解析

4.4.1反汇编指令

反汇编指令:objdump -d -r hello.o >hello_o_asm

4.4.2 反汇编文件

反汇编文件的内容如下:

hello.o:     文件格式 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 04              cmpl   $0x4,-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 48                jmp    80 <main+0x80>

  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+0x22

  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:    48 8b 45 e0             mov    -0x20(%rbp),%rax

  66:    48 83 c0 18             add    $0x18,%rax

  6a:    48 8b 00               mov    (%rax),%rax

  6d:    48 89 c7               mov    %rax,%rdi

  70:    e8 00 00 00 00           callq  75 <main+0x75>

                    71: R_X86_64_PLT32      atoi-0x4

  75:    89 c7                mov    %eax,%edi

  77:    e8 00 00 00 00           callq  7c <main+0x7c>

                    78: R_X86_64_PLT32      sleep-0x4

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

  80:    83 7d fc 08              cmpl   $0x8,-0x4(%rbp)

  84:    7e b2                jle    38 <main+0x38>

  86:    e8 00 00 00 00           callq  8b <main+0x8b>

                    87: R_X86_64_PLT32      getchar-0x4

  8b:    b8 00 00 00 00           mov    $0x0,%eax

  90:    c9                   leaveq

  91:     c3                   retq  

对照反汇编的代码和hello.s的指令,我们可以发现以下几点区别:

1.数字的表示不同:hello.s中的数据表示使用的是十进制,而hello.o的反汇编代码的操作数使用的是16进制。

2.分支转移:hello.s中的分支使用的是符号,比如.L1, .L3等,而反汇编代码中使用的是相对偏移的地址,比如jle 38<main+0x38>,使用的是相对于main函数的偏移量。

3.函数调用:hello.s中函数调用使用的是函数的名称,比如call printf@PLT。

而hello.o反汇编代码中函数使用相对main函数的偏移量来表示应该填入函数地址的位置。这个位置目前是空的,需要链接时才能确定所需函数的位置。同时在反汇编代码中给出了函数的寻址方式:PC相对寻址。

4.5 本章小结

本章介绍了汇编这一操作。有编译得到的hello.s文件是文本文件,机器还是无法识别,而汇编将文本文件转化为二进制的可重定位目标文件hello.o,这样机器就可以识别了。一个.c文件会生成一个.o的可重定位目标文件。

Elf中记录着可重定位目标文件的许多信息,包括节的信息、符号的信息等,这些为链接生成可执行目标文件做了准备。另外,我们将hello.s与hello.o的反汇编代码进行了对比,发现两者有很大的相似之处,但是在数字的表示、跳转和函数调用等方面存在一定的区别。

(第41分)

第5章 链接

5.1 链接的概念与作用

链接的概念:

链接(linking)是将各种代码和数据片段收集和组合成为一个单一文件的过程,这个文件可悲加载(复制)到内存并执行。链接可以执行与编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是程序被加载器加载到内存并执行时;甚至执行于运行时,也就是有应用程序来执行。在早期的计算机系统中,链接时手动执行的。在现代系统中,链接是由叫做链接器 的程序自动执行的。

链接器的作用

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

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

 图20:链接命令

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

命令:readelf -a hello>hello_elf

(1)ELF文件头

可执行目标文件的elf头和可重定位目标文件的elf头非常类似。Elf头中包含了魔数,文件类别,数据表示方式,版本号,程序头起点,elf头的大小,节的大小、数量,节头表的索引等信息。

Hello_elf的ELF如下:

图21:hello_elf的文件头

(2)节头

描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

图22:hello_elf的节头

(3)程序头表

程序头表是一个结构数组,反映可执行文件的连续的片被映射到连续的内存段的映射关系。

图23:hello_elf的程序头表

(4)节段映射

节段映射,说明了在链接过程中,将多个代码段与数据段分别合并成一个单独的代码段和数据段,并根据段的大小以及偏移量重新设置各个符号的地址。

图24:hello_elf的节段映射

(5)重定位节

对于可执行目标文件来说,仍然会存在重定位信息,因为有些需要动态链接的块还没有被链接,重定位节中就给出了这些符号的相关信息。

(6)符号表

对于可执行目标文件来说,包含两个符号表,一个符号表的名称为.dynsym, 从名称和符号表中的内容来看应该是还没有动态链接的一些未知符号。另一张符 号表就是熟知的.symtab,里面保存了程序中定义和引用的函数以及全局变量等的 信息。

5.4 hello的虚拟地址空间

在5.3节中我们分析了hello_elf,hello_elf的文件头中指出程序的起点是4010f0,我们使用edb打开可执行目标文件hello,在Data Dump中我们可以看到4010f0的位置处存储着有效信息。

图25:edb中Data Dump显示的程序起点

类似的,在hello_elf的节头中我们可以看到rodata从402000开始,在edb中我们可以看到printf的格式输入字符串“Hello %s %s\n”在地址为40202e的地址处。

图26:printf的格式输入串

进程的虚拟映射空间用下面这张图可以很好地表示

图27:Linux进程的虚拟地址空间

5.5 链接的重定位过程分析

命令:objdump -d -r hello > hello_asm

分析hello和hello.o的区别:

1.链接增加新的函数:

在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。因此hello反汇编出的文件内容更多。

图28:hello_asm中plt节中的函数

2.增加的节:

hello中增加了.init和.plt节。其中init节会对程序进行初始化。

图29:hello_asm中init节

3.函数调用:

对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。函数调用call后面都是用0来填充。而hello中没有hello.o中的重定位条目,并且跳转和函数调用的地址都变成了虚拟内存地址。

4.地址访问:

hello.o中的相对偏移地址变成了hello中的虚拟内存地址。hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时才确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

链接的过程:

根据hello和hello.o的不同,分析出链接的过程为:

链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则合并在一起。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

子函数名和地址(后6位)

401000 <_init>

401020 <.plt>

401030 <puts@plt>

401040 <printf@plt>

401050 <getchar@plt>

401060 <atoi@plt>

401070 <exit@plt>

401080 <sleep@plt>

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1 <main>

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>

图30:edb运行程序

5.7 Hello的动态链接分析

书上的解释是这样的:假设程序调用一个有共享库定义的函数。编译器无法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法时为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU通过一种叫做延迟绑定(lazy binding)的技术来将地址的绑定推迟到第一次调用该过程。

动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。

图31:执行init之前PLT中的内容

图32:执行init之后PLT中的内容

初始时GOT里面存的都是PLT的第二条指令,随后链接器修改GOT,下一次再调用PLT时,指向的就是正确的内存地址。PLT就能跳转到正确的区域。

5.8 本章小结

本章通过hello程序简单介绍了链接。链接主要两个步骤是符号解析和重定位,将不同的可重定位目标文件链接成一个完整的可以载入内存执行的程序。通过分析hello.o和hello的反汇编代码,更好地理解了链接中的重定位和以及虚拟内存映射。通过edb对程序的调试,了解了在hello程序执行前、后需要运行的系统函数,以及动态链接所要做的工作。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念:

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

6.1.2进程的作用:

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

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

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

6.2.1 Shell的概念

shell是一个交互型的应用级程序,它代表客户运行其他程序。

6.2.2 Shell的作用

Shell连接了用户和Linux内核,让用户能够更加高效、安全、低成本地使用Linux内核,能够接收用户输入的命令,并对命令进行处理,处理完毕后再将结果反馈给用户,比如输出到显示器、写入到文件等。

在 Shell 中输入的命令,有一部分是 Shell本身自带的,这叫做内置命令;有一部分是其它的应用程序(一个程序就是一个命令),这叫做外部命令。

Shell 本身支持的命令并不多,功能也有限,但是 Shell可以调用其他的程序,每个程序就是一个命令,这使得 Shell命令的数量可以无限扩展,其结果就是 Shell 的功能非常强大,完全能够胜任 Linux的日常管理工作,如文本或字符串检索、文件的查找或创建、大规模软件的自动部署、更改系统设置、监控服务器性能、发送报警邮件、抓取网页内容、压缩文件等。

6.2.3 Shell的处理流程

Shell先分词,判断命令是否为内部命令,如果不是,则寻找可执行文件进行执行,重复这个流程:

  1. Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
  2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
  3. 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
  4. Shell对~符号进行替换。
  5. Shell对所有前面带有$符号的变量进行替换。
  6. Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
  7. Shell计算采用$(expression)标记的算术表达式。
  8. Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
  9. Shell执行通配符* ? [ ]的替换。
  10. Shell把注释删除,并且按照下面的顺序实行命令的检查:
  1. 内建的命令
  2. shell函数(由用户自己定义的)
  3. 可执行的脚本文件(需要寻找文件和PATH路径)
  1. 在执行前的最后一步是初始化所有的输入输出重定向。
  2. 最后执行命令。

6.3 Hello的fork进程创建过程

首先Shell会检查hello程序是否一个内置命令。当发现不是内置命令时,则执行这个程序。父进程通过fork函数创建进程,子进程得到一份和父进程虚拟空间相同但是相互独立的副本——包括数据段、代码段、共享库、堆、用户栈等。子进程和父进程的差异在于两者的PID不同。

Fork函数是一个比较特殊的函数,它调用1次,返回2次,在父进程中fork返回子进程的PID,在子进程中fork返回0。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。该函数的作用是在当前进程的上下文中加载并运行一个新的程序。

execve函数正常运行不返回,只有当出现错误时,例如找不到Hello时,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.5 Hello的进程执行

(1)逻辑控制流

一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。

(2)用户模式和内核模式

处理器使用一个寄存器提供两种模式的区分:用户模式和内核模式。

用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据。

内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

(3)上下文

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

图33 进程的上下文切换

以上图为例,进程在执行到read指令时需要磁盘的读写,而磁盘的读写的时间是毫秒级的,如果不进行任何操作的话会浪费几十万个时钟周期,因此处理器会进行进程的上下文切换,将进程A的上下文保存起来,在磁盘读取的同时,转到进程B中去执行操作,当磁盘的读写完成的时候再转回到进程A中,恢复之前保存的上下文,继续之前未执行的操作。

(4)调度的过程

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。

(5)用户态与核心态转换

为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理

6.6.1正常运行状态:

图34:Hello正常运行的状态

6.6.2异常的类型

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

6.6.3异常的处理方式

图35 中断处理方式

图 36 陷阱处理方式

图 37 故障处理方式

图 38 终止处理方式

6.6.4 Hello程序的异常处理

(1)Ctrl+C

在程序运行的时候从键盘输入Ctrl+C,程序会受到SIGINT信号,从而进程会终止。

图39:进程被Ctrl+C终止

(2)中途乱按

在程序运行的时候乱按键盘,从键盘输入的字符会在屏幕上显示出来。在bash中乱按,bash会检查输入是否是内置命令或是可执行的程序,如果不是会显示这不是内置命令。

图40:中途乱按键盘

(3)Ctrl+Z

在程序运行的时候键入ctrl+Z,进程会收到SIGTSTP的信号,hello进程因此被挂起。可以使用ps命令来查看hello进程的PID。

ps能列出当前的所有进程,pstree能列出进程树,jobs能显示当前暂停的进程,fg使hello在前台继续进行,kill能给hello发送指定的信号。

图41:键入Ctrl+Z后使用ps和pstree操作

图42:hello进程挂起后执行jobs和fg命令

Kill命令可以给指定的进程发送信号

Kill -9 87682 能够将PID为87682的hello进程终止。

图43:kill命令发送信号

6.7本章小结

之前的章节我们了解到是程序,程序是静态的,而进程是执行的程序,进程是动态的。进程通过父进程使用fork函数获得了生命,使用execve函数实现了进程的加载。计算机中会有许多进程同时在运行,内核调度程序会进行进程的调度,实现系统效率的优化。在进程调度的过程中,会有用户模式和内核模式的切换,上下文的切换、保存。

同时我们介绍了四种异常类型,以及信号机制。不同的异常对应不同的处理机制。同理对于不同的信号,系统会有不同的处理程序。我们可以通过给进程发送信号的方式来对进程进行管理操作。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址

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

  1. 线性地址

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

  1. 虚拟地址

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

  1. 物理地址

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

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
    索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
    这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。
    全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
    GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
   给定一个完整的逻辑地址段选择符+段内偏移地址,
   看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
   拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

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

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

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

图 44 页式管理流程图

优点:

  1. 由于它不要求作业或进程的程序段和数据在内存中连续存放,从而有效地解决了碎片问题。
  2. 动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。

缺点:

  1. 要求有相应的硬件支持。例如地址变换机构,缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本。
  2. 增加了系统开销,例如缺页中断处理机,
  3. 请求调页的算法如选择不当,有可能产生抖动现象。
  4. 虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到利用果页面较大,则这一部分的损失仍然较大。

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

多级页表:

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

图 45 使用k级页表进行翻译

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

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

CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CS(组号),CO(偏移量)。根据CS寻找到正确的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。

图46 3级Cache

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。

它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。

7.7 hello进程execve时的内存映射

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

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

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

3)删除已存在的用户区域。

4)映射私有区域

5)映射共享区域

6)设置程序计数器(PC)

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

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:

图 47 缺页中断处理

整体的处理流程:

  1. 处理器生成一个虚拟地址,并将它传送给MMU
  2. MMU生成PTE地址,并从高速缓存/主存请求得到它
  3. 高速缓存/主存向MMU返回PTE
  4. PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
  5. 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
  6. 缺页处理程序页面调入新的页面,并更新内存中的PTE
  7. 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

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

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

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

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

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

显示空间链表管理

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

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

7.10本章小结

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

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

文件(所有的I/O设备都被模型化为文件,甚至内核也被映射为文件)

设备管理:unix io接口

这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。

我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等

8.2 简述Unix IO接口及其函数

Unix IO接口:

打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。

Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。

改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。

读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k

关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中

Unix IO函数:

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操作,应该读多少数量的字符。

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

4. write()函数

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

所需头文件: #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;

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

    i = vsprintf(buf, fmt, arg);

    write(buf, i);

    return i;

}

所引用的vsprintf函数

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

{

    char *p;

    chartmp[256];

    va_listp_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函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

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

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

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

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

8.5本章小结

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

(第81分)

结论

我们最早接触的hello程序从高级语言C程序到可见的执行结果经历的步骤远比我们想象中的要复杂。对hello一生的漫游,就是对计算机系统的一次漫游。

  1. hello.c经过预处理,拓展得到hello.i文本文件
  2. hello.i经过编译,得到汇编代码hello.s汇编文件
  3. hello.s经过汇编,得到二进制可重定位目标文件hello.o
  4. hello.o经过链接,生成了可执行文件hello
  5. bash进程调用fork函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello
  6. hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址。
  7. hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关
  8. hello最终被shell父进程回收,内核会收回为其创建的所有信息

通过本学期对计算机系统的学习和实验的实践,我对计算机系统中一些底层的实现有了更加清晰的认识,比如数据的表示、程序的机器织表示、流水线的原理、编译原理、链接、信号处理等。CSAPP一书深入浅出,在阅读过程中,自己的思维得到了提升。

在今后计算机的学习中,动手实践和理论学习中应当相互促进,一起精进。

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

附件

文件的作用

文件名

源文件

hello.c

预处理后的文件

hello.i

编译之后的汇编文件

hello.s

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

hello.o

链接之后的可执行目标文件

hello

hello.o 的 ELF 格式

hello_o_elf.txt

hello.o 的反汇编代码

hello_o_asm

hello的ELF 格式

hello_elf

hello 的反汇编代码

hello_asm

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

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

参考文献

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

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

[2]  CSDN博客 Ubuntu系统预处理、汇编、链接指令

[3]  CSDN博客 ELF可重定位目标文件格式

[4]  博客园 shell命令执行过程

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

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值