本篇论文将从计算机系统课程中的所学内容通过hello小程序的一生,即从hello.c程序编写完成到hello预处理、编译、汇编、链接最后被回收结束该过程,来对所学知识内容进行全面的梳理与回顾。我主要在Linux系统下的Ubuntu20.04操作系统中进行内存管理、进程管理、I/O管理、虚拟内存以及异常信号等相关操作,在合理运用了Ubuntu下的一些操作工具,并且进行了细致的历程分析。通过勾勒出hello完整坎坷却不失华丽的一生,目的是为了加深自己对计算机系统的理解,同时从计算机系统层面上在各个方面游览了hello程序的生命周期,揭开了它的神秘面纱。
关键词:预处理;Linux;020;P2P;Ubuntu;汇编;链接;计算机系统
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
1.1.1 P2P
Program:在编译中键入敲出代码得到hello.c程序时,我们敲击键盘的过程中,字符被读入寄存器,然后被存入内存中。当我们保存文件并退出,程序文本被交换到了磁盘里。
Process: hello.c在Linux系统中运行shell程序输入命令gcc hello.c -o hello后经过cpp的预处理、ccl的编译、as的汇编以及ld的链接最终成为了可执目标程序文件hello。继而在shell中键入启动命令后,shell为其创建子进程(fork),产生子进程,接着把hello的内容加载到子进程的地址空间,当子进程执行完return语句后,它保持已终止状态,此时向shell发送sigchild信号,等待shell对其进行回收。当shell调用waitpid指示操作系统将其回收后,hello的生命周期便结束了。
1.1.2 020
shell为hello进程execve来映射虚拟内存,进入程序入口后程序才开始载入物理内存。进入main()函数执行其目标代码, CPU则为运行的hello分配时间片从而执行逻辑控制流。当hello程序运行结束后,shell的父进程便负责回收hello进程,最后内核删除相关数据结构。
1.2 环境与工具
1.2.1硬件环境
处理器:12th Gen Intel(R) Core(TM) i7-12700H 2.70 GHz
RAM:16.00GB
系统类型:64位操作系统,基于x64的处理器
1.2.2软件环境
Windows11 64位;Ubuntu 20.04
1.2.3开发与调试工具
gcc,as,ld,vim,edb,readelf,visual studio
1.3 中间结果
文件的名字 | |
经过预处理后的hello源程序文本文件 | hello.i |
经过编译之后产生的汇编文件文本文件 | hello.s |
经过汇编之后产生的可重定位目标文件二进制文件 | hello.o |
通过链接产生的可执行目标文件二进制文件 | hello |
hello.o的ELF格式文本文件 | elf.txt |
hello.o的反汇编代码文本文件 | Disas_hello.s |
hello的ELF格式文本文件 | hello1.elf |
hello的反汇编代码文本文件 | hello1_objdump.s |
1.4 本章小结
本章是对hello程序进行了一个总体详细而简明的概括,首先介绍P2P、020的步骤过程及其意义,然后又介绍大作业中我所需使用的硬件环境、软件环境和开发调试工具,最后则简述从hello.c文件到hello可执行文件所见所经历的过程中产生的中间结果。众所周知Hello World程序基本上是所有的程序员在学习第一门程序语言时所编写的第一个程序,接下来我们将继续探究该程序的一生,从各个方面了解和理解其执行细节。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念:
预处理(preprocessing)指的是预处理器(cpp)根据以字符#开头的命令,来修改原始的C程序,最后生成.i文本文件的过程。预处理中首先会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中关于ISO C/C++要求支持的包括有#if、#ifdef、#ifndef、#else、#elif、#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
2.1.2预处理的作用:
- 将源文件中用#include 形式声明的文件复制到新的程序中。例如hello.c文件中第6-8行的#include<stdio.h>等命令使得预处理器读取系统头文件stdio.h、unistd.h和stdlib.h的内容,并将其直接插入到程序文本中;
- 用实际值替换用#define 定义的字符串;
- 根据#if后面的条件决定需要编译的代码;
- 预编译程序可以识别一些特殊符号,预编译程序对于在源程序中出现的这些特殊符号串会用合适值进行替换。
- 预处理还可以帮助程序员节省工作量,提高程序可读性,便于维护。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
图1 Ubuntu下预处理命令
2.3 Hello的预处理结果解析
此时我发现程序共拓展成了3060行,原始hello.c程序出现在3046行之后。这之前是头文件stdio.h、unistd.h以及stdlib.h依次展开。其中以stdio.h展开为例: stdio.h是标准库文件,预处理cpp需要在Ubuntu中默认的环境变量下寻找stdio.h,打开文件/usr/include/stdio.h,可以发现其中依然使用了#define宏定义语句,cpp对stdio中的宏定义是递归展开的,所以最终的.i文件中是没有#define的,但是其中却使用了大量的#ifdef 以及#ifndef 条件编译语句,cpp会对条件值进行判断来决定是否执行包含在其中的条件逻辑。同时之前提到过,预编译程序可识别一些特殊符号,预编译程序对于在源程序中出现的这些特殊符号串会用合适值进行替换。
hello.i插入文件库所在位置:
图2 hello.i库文件部分
hello.i库中预置声明函数位置:
图3 hello.i库中预置声明函数部分
hello.i源代码位置:
图4 hello.i源代码部分
2.4 本章小结
本章主要是对预处理(包括头文件展开、宏替换、去掉注释、条件编译等等)的概念和作用进行介绍,除此之外还有Ubuntu下预处理的两个指令,同时也具体到了我的hello.c文件预处理结果hello.i文本文件的详细解析,深刻理解了预处理的重要内涵。我知晓了预处理可以最大程度上减轻程序员的负担,提供一定的可移植性:通过宏定义(#define)可以更简明地定义一些实用“函数”,再通过各种编译命令,将所写代码根据不同运行的平台进行调整,从而避免不必要麻烦。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.2编译的概念:
编译程序是通过词法分析和语法分析,在确认所有的指令都符合语法规则后,将其翻译成等价的中间代码或汇编代码来表示。编译器(cc1)将预处理过的文本文件hello.i 翻译转换成汇编文本文件hello.s。
3.1.2编译的作用:
以下为编译基本流程:
1.语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成了符合语法规则的语法单位,方法有自上而下分析法和自下而上分析法两种;
2.中间代码:源程序的一种内部表示,或称之为中间语言。中间代码的作用是使编译程序的结构在逻辑上更为简单明确,特别是使目标代码的优化比较容易实现;
3.代码优化:指对程序进行多种等价变换,使得从变换后的程序出发时能生成更有效的目标代码;
4.目标代码:生成目标代码是编译的最后阶段。目标代码生成器把语法分析或优化后生成的中间代码转换成目标代码。此处目标代码指汇编语言代码,即不同种类的语言提供相同的形式,其指令与处理器的指令集类似,更贴近底层,便于汇编器将其转换为可执行机器语言代码供机器执行。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图5 Ubuntu下编译命令
3.3 Hello的编译结果解析
3.3.1.1常量
在if语句if (argc != 4)中,常量4的值保存的位置在.text中,作为指令的一部分:movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
同理可得循环:
for (i = 0; i < 8; i++) {
printf("Hello %s %s\n", argv[1], argv[2]);
sleep(atoi(argv[3]));
}
中的数字0、8、1、2、3也是被存储在.text中;
在下述函数中:
printf("用法: Hello 7203610121 刘天瑞 599184000s!\n");
printf()中的字符串则被存储在.rodata中:
.LC0:
.string "\323\303\267\250: Hello 7203610121 \301\365\314\354\310\360 599184000s\243\241"
3.3.1.2变量
全局变量:
初始化全局变量储存在.data中,其初始化不需要汇编语句而可以直接完成。
局部变量:
局部变量存储在寄存器或栈中,程序中局部变量由i定义:
int i;
在汇编代码中:
.L2:
movl $0, -4(%rbp)
jmp .L3
此处是循环前i=0的操作,i被保存在栈当中的%rsp-4位置上。
3.3.2 算术操作
在循环操作中使用自加++操作符:for (i = 0; i < 8; i++)
即在每次循环执行的内容结束后,对i进行一次自加,栈上存储变量i的值加1: addl $1, -4(%rbp)
3.3.3 控制转移/关系操作
程序第13行中判断传入参数argc是否等于4,源代码为:
if (argc != 4) {
printf("用法: Hello 7203610121 刘天瑞 599184000s!\n");
exit(1);
}
汇编代码为:
cmpl $4, -20(%rbp)
je .L2
其中je判断cmpl是否产生条件码,如果两个操作数的值不相等则跳转至指定地址;
for循环中的循环执行条件:for (i = 0; i < 8; i++)
汇编代码为:
.L3:
cmpl $7, -4(%rbp)
jle .L4
其中jle判断cmpl是否产生条件码,如果后一个操作数的值小于或等于前一个操作数的值则跳转至指定地址。
3.3.4 数组/指针/结构操作
主函数main()的参数中存在指针数组char *argv[]:
int main(int argc, char* argv[]) {}
在argv[]数组中,易知argv[0]指向输入程序数组的路径和名称,argv[1]和argv[2]则分别表示两个字符串。char* 数据类型占8个字节:
.LFB6:
movl %edi, -20(%rbp)//argc存储在%edi
movq %rsi, -32(%rbp)//argv存储在%rsi
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
.L4:
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
.LC1:
.string "Hello %s %s\n"
.text
.globl main
.type main, @function
通过对比原函数可知,%rsi-8和%rax-16分别得到了argv[1]和argv[2]两个字符串。
3.3.5 函数操作
在x86-64中有过程调用传递参数的规则如下:
1~6参数依次存储在%rdi、%rsi、%rdx、%rcx、%r8、%r9六个寄存器中,剩下的参数则可以直接存储在栈当中。
下面介绍各类函数:
main函数:
参数传递:输入参数argc和argv[],分别存储在寄存器%rdi和%rsi中;
函数调用:被系统启动的函数调用;
函数返回:设置%eax=0并返回,对应为:return 0 ;
源代码如下:
int main(int argc, char* argv[])
汇编代码如下:
main:
.LFB6:
.cfi_startproc
endbr64
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)
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
可以发现argc存储在%edi中,存储在%rsi中;
printf函数:
参数传递:call puts时只输入字符串参数首地址,for循环中call printf时输入 argv[1]和argc[2]的地址;
函数调用:if判断满足条件后在for循环中被调用;
源代码1如下:
printf("用法: Hello 7203610121 刘天瑞 599184000s!\n")
汇编代码1如下:
.LC0:
.string "\323\303\267\250: Hello 7203610121 \301\365\314\354\310\360 599184000s\243\241"
.LFB6:
.cfi_startproc
endbr64
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)
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@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
exit函数:
参数传递:输入参数为1后再执行退出命令;
函数调用:if判断条件满足后被调用;
源代码如下:exit(1);
汇编代码如下:
.LFB6:
movl $1, %edi
call exit@PLT
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
其中函数atoi()(表示为ascii to integer)是把字符串转换成整型数的一个函数,多应用在计算机程序和办公软件中。例如int atoi(const char *nptr) 函数会扫描参数 nptr字符串,跳过前面的空白字符(例如空格,tab缩进)等。如果 nptr不能转换成 int 或者 nptr为空字符串,那么将返回0。特别注意该函数要求被转换的字符串是按十进制数理解的。
getchar函数:
函数调用:在main中被调用,call getchar
源代码如下:getchar();
汇编代码如下:
.L3:
cmpl $7, -4(%rbp)
jle .L4
call getchar@PLT
3.4 本章小结
本章主要是对编译概念和作用进行详细介绍,同时也通过示例函数表明c语言是如何转换成为汇编代码的。除此之外我还介绍了汇编代码是如何实现变量、常量、传递参数、分支以及循环。编译程序所做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则后,将其翻译成等价的中间代码或汇编代码来表示。包括之前对编译结果进行详尽仔细地解析,都使得我更加深刻理解C语言数据与操作,对C语言编译成汇编语言有更为恰当的掌握。因为汇编语言具有通用性,我掌握了它也相当于掌握了语言间的一些共性。汇编是C语言源程序经过预处理器、编译器处理后的结果,也是最贴近机器底层的语言。即使在高级语言盛行的今天,仍然需要学习汇编并掌握它。最后,通过学习汇编可以理解编译器的优化性能,并且分析解析出代码中所隐含的低效率从而更好地优化程序。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
驱动程序运行汇编器将汇编语言(hello.s)翻译成机器语言(hello.o)的过程称为汇编,同时这个机器语言文件是可重定位目标文件。汇编器接受.s文件作为输入,以.o可重定位目标文件作为输出。可重定位目标文件包含二进制代码和数据,在编译时与其他可重定位目标文件能够合并起来,创建成一个可执行目标文件,从而被加载到内存中执行。
4.1.2 汇编的作用
汇编是将高级语言转化为机器可以直接识别并且执行的代码文件的过程,汇编器将.s文件汇编程序翻译成机器语言指令,并将这些指令打包成为可重定位目标程序的格式,最后将结果保留在.o目标文件中,.o文件是一个包含程序指令编码的二进制文件。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
图6 Ubuntu下汇编命令
4.3 可重定位目标elf格式
4.3.1 导出elf文件命令
命令: readelf -a hello.o > ./elf.txt
图7 Ubuntu下生成并导出elf文件命令
4.3.2 ELF头
其中包含了系统信息,编码方式,ELF头大小,节的大小和数量等一系列必要有效信息。描述生成该文件系统字节大小和字节顺序、帮助链接器语法分析以及解释目标文件的有效信息。 ELF头的主要内容如下所示:
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 1248 (bytes into file)
标志: 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
4.3.3 ELF节头
头节目表描述了.o文件中出现的各个节的类型、位置、所占空间大小等必要有效信息。如下所示,其属性分别有名称、类型、地址(此时暂时未被分配均为0)、偏移量(节相对于文件开始的偏移)、节大小、全体(Entry)大小、旗标(节属性)、链接(与其他节的)、信息(附加节)、对齐(2的Align次方)。
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000092 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000390
00000000000000c0 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000d2
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000d2
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000d8
0000000000000038 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 00000110
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000013c
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 00000140
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000160
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000450
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000198
00000000000001b0 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000348
0000000000000048 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000468
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)
4.3.4 重定位节
这其中表述了各个段引用的外部符号等必要有效信息。在链接时需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断通过偏移量等信息应使用什么方法计算出正确地址值。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。
重定位节 '.rela.text' at offset 0x390 contains 8 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000021 000c00000004 R_X86_64_PLT32 0000000000000000 puts - 4
00000000002b 000d00000004 R_X86_64_PLT32 0000000000000000 exit - 4
000000000054 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 27
00000000005e 000e00000004 R_X86_64_PLT32 0000000000000000 printf - 4
000000000071 000f00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
000000000078 001000000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000087 001100000004 R_X86_64_PLT32 0000000000000000 getchar - 4
重定位节 '.rela.eh_frame' at offset 0x450 contains 1 entry:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
4.3.5 符号表
符号表是用来存放在程序中被定义和引用的函数和全局变量的必要有效信息。其中的puts、exit等C标准函数库中的函数都是全局符号(GLOBAL)。Value是该符号在对应节中的偏移量。
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 146 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > Disas_hello.s
图8 Ubuntu下反汇编命令
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+0x27
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 07 cmpl $0x7,-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
从以上反汇编代码文件能够发现反汇编中最为明显的区别便是调用函数中并未填入有效数字,例如第16行处从第二个字节开始的是四个字节的0。这些字节需要留到链接阶段进行重定位符号引用才可以填入相对偏移量。除此之外hello.o的反汇编代码中的操作数是十六进制,尤其在分支转移函数中,反汇编代码的跳转指令后是相对偏移的间接地址,与汇编语言的十进制操作数是不一致的。由于在编译阶段没有保留符号的名字,函数调用均写成<main+offset>的形式。
汇编代码hello.s中函数调用的call指令使用的是函数名称,但是反汇编代码中的call指令使用的是main函数相对偏移地址。由于函数只有在链接后才能确定所运行执行的地址,因此也需要为其添加重定位条目。
对机器语言所使用特定字节表示各类操作进行总结概括:给定文件开始位置就能够将合法字节序列唯一地翻译解释成有效指令;而汇编语言的操作数直接使用程序员就可以读懂的字符(例如%rax、$10)来表示;但是机器反汇编代码的操作数却会被映射为特定字节(针对所使用的寄存器)或“大/小端法”来表示的十六进制直接数。
4.5 本章小结
本章我对汇编与反汇编结果进行了详尽的介绍。经过汇编器的一系列操作,汇编语言可以转化为机器语言(反汇编),生成了hello.o可重定位目标文件是为后面的链接任务做好铺垫。接着我通过对比hello.s汇编代码和hello.o反汇编代码的区别,更为深刻地理解了汇编语言是如何实现转变到机器语言以及该过程中为链接需要的准备工作。同时我也对可重定位目标elf格式文件进行了细致的考察,主要侧重点是重定位项目以及对hello.o文件的反汇编,并且将Disas_hello.s文件与第3章所生成的hello.s文件也进行了比较得到如下总结:汇编器接受汇编代码,产生可重定位目标文件,它能够与其他可重定位目标文件合并产生一个直接加载被运行的可执行目标文件。我在第5章会说明如何确定最终符号的最终运行位置如何,将多个可重定位目标文件合并。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是指将多种不同重定位目标文件或静态/动态库的代码和数据部分收集整合起来,修改符号引用,组合并且输出成一个单一可执行目标文件的过程。
链接的作用:
链接能够节省源程序空间,同时对于未编入的常用函数文件能够进行合并从而生成能够正常工作的可执行目标文件。除此之外链接也可以表述为使得一个复杂程序被分解成诸多简明模块来进行编写,并且最终被合并为一个可执行程序。这些作用使得分离编译成为了一种可能。
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
图9 Ubuntu下链接命令
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello.elf
图10 Ubuntu下生成并导出hello的elf文件命令
5.3.1 ELF头:
文件头中类型由REL(可重定向文件)变为EXEC (可执行文件);入口点地址由0x0(未确定)变为了0x4010f0(确定);程序和节的起始位置和大小都有变化;节个数由14个变为了27个。
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x4010f0
程序头起点: 64 (bytes into file)
Start of section headers: 14208 (bytes into file)
标志: 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
5.3.2 节头:
节的数目有显著增加变化,由14变为27。多出来的部分节的主要作用我在CSAPP的第490页查阅到如下所示:
.interp: 包含了动态链接器在文件系统中的路径;
.note.ABI-tag: ELF规范中记录的注释部分,包含一些版本信息;
.gnu.hash: 符号的哈希表,用于加速查找符号;
.dynamic,.dynsym,.dynstr: 与动态链接符号相关;
.gnu.version,.gnu.version_r: 与版本有关的信息;
.init_array,.fini_array: 存放函数指针,其中的函数分别在main函数之前之后调用,用于初始化和收尾;
.init,.fini: 存放上述初始化和收尾的代码。
.eh_frame_hdr: 与异常处理相关。
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 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
0000000000000038 0000000000000004 A 6 0 8
[ 5] .gnu.hash GNU_HASH 0000000000400378 00000378
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000400398 00000398
00000000000000d8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000400470 00000470
000000000000005c 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000004004cc 000004cc
0000000000000012 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 00000000004004e0 000004e0
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000400500 00000500
0000000000000030 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000400530 00000530
0000000000000090 0000000000000018 AI 6 21 8
[12] .init PROGBITS 0000000000401000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000401020 00001020
0000000000000070 0000000000000010 AX 0 0 16
[14] .plt.sec PROGBITS 0000000000401090 00001090
0000000000000060 0000000000000010 AX 0 0 16
[15] .text PROGBITS 00000000004010f0 000010f0
0000000000000145 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 0000000000401238 00001238
000000000000000d 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000402000 00002000
0000000000000040 0000000000000000 A 0 0 8
[18] .eh_frame PROGBITS 0000000000402040 00002040
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
0000000000000048 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000404048 00003048
0000000000000004 0000000000000000 WA 0 0 1
[23] .comment PROGBITS 0000000000000000 0000304c
000000000000002b 0000000000000001 MS 0 0 1
[24] .symtab SYMTAB 0000000000000000 00003078
00000000000004c8 0000000000000018 25 30 8
[25] .strtab STRTAB 0000000000000000 00003540
0000000000000158 0000000000000000 0 0 1
[26] .shstrtab STRTAB 0000000000000000 00003698
00000000000000e1 0000000000000000 0 0 1
5.3.3 符号表:
能够发现可执行文件中的符号表多出很多符号,并且额外又多出一个动态符号表(.dynsym),其中printf()、puts()、atoi()、exit()、getchar()等C标准库函数在动态符号表和符号表中都有表项。除此此外这些符号已确定好了运行时所处的位置,这和可重定向文件是截然不同的。
Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@GLIBC_2.2.5 (2)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 51 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004002e0 0 SECTION LOCAL DEFAULT 1
2: 0000000000400300 0 SECTION LOCAL DEFAULT 2
3: 0000000000400320 0 SECTION LOCAL DEFAULT 3
4: 0000000000400340 0 SECTION LOCAL DEFAULT 4
5: 0000000000400378 0 SECTION LOCAL DEFAULT 5
6: 0000000000400398 0 SECTION LOCAL DEFAULT 6
7: 0000000000400470 0 SECTION LOCAL DEFAULT 7
8: 00000000004004cc 0 SECTION LOCAL DEFAULT 8
9: 00000000004004e0 0 SECTION LOCAL DEFAULT 9
10: 0000000000400500 0 SECTION LOCAL DEFAULT 10
11: 0000000000400530 0 SECTION LOCAL DEFAULT 11
12: 0000000000401000 0 SECTION LOCAL DEFAULT 12
13: 0000000000401020 0 SECTION LOCAL DEFAULT 13
14: 0000000000401090 0 SECTION LOCAL DEFAULT 14
15: 00000000004010f0 0 SECTION LOCAL DEFAULT 15
16: 0000000000401238 0 SECTION LOCAL DEFAULT 16
17: 0000000000402000 0 SECTION LOCAL DEFAULT 17
18: 0000000000402040 0 SECTION LOCAL DEFAULT 18
19: 0000000000403e50 0 SECTION LOCAL DEFAULT 19
20: 0000000000403ff0 0 SECTION LOCAL DEFAULT 20
21: 0000000000404000 0 SECTION LOCAL DEFAULT 21
22: 0000000000404048 0 SECTION LOCAL DEFAULT 22
23: 0000000000000000 0 SECTION LOCAL DEFAULT 23
24: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
25: 0000000000000000 0 FILE LOCAL DEFAULT ABS
26: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
27: 0000000000403e50 0 OBJECT LOCAL DEFAULT 19 _DYNAMIC
28: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
29: 0000000000404000 0 OBJECT LOCAL DEFAULT 21 _GLOBAL_OFFSET_TABLE_
30: 0000000000401230 5 FUNC GLOBAL DEFAULT 15 __libc_csu_fini
31: 0000000000404048 0 NOTYPE WEAK DEFAULT 22 data_start
32: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
33: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 _edata
34: 0000000000401238 0 FUNC GLOBAL HIDDEN 16 _fini
35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
36: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
37: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 22 __data_start
38: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@@GLIBC_2.2.5
39: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
40: 0000000000402000 4 OBJECT GLOBAL DEFAULT 17 _IO_stdin_used
41: 00000000004011c0 101 FUNC GLOBAL DEFAULT 15 __libc_csu_init
42: 0000000000404050 0 NOTYPE GLOBAL DEFAULT 22 _end
43: 0000000000401120 5 FUNC GLOBAL HIDDEN 15 _dl_relocate_static_pie
44: 00000000004010f0 47 FUNC GLOBAL DEFAULT 15 _start
45: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 __bss_start
46: 0000000000401125 146 FUNC GLOBAL DEFAULT 15 main
47: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@@GLIBC_2.2.5
48: 0000000000000000 0 FUNC GLOBAL DEFAULT UND
t@@GLIBC_2.2.5
49: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@@GLIBC_2.2.5
50: 0000000000401000 0 FUNC GLOBAL HIDDEN 12 _init
5.4 hello的虚拟地址空间
图11 Linux进程虚拟地址空间的典型布局
使用edb加载hello,其中的Data Dump窗口能够查看到加载到虚拟地址中的hello程序:
图12 edb中的Data Dump视图
查看 ELF 格式文件中的程序头,如下所示:
程序头:
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
0x00000000000005c0 0x00000000000005c0 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000245 0x0000000000000245 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x000000000000013c 0x000000000000013c R 0x1000
LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001fc 0x00000000000001fc 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 ,DYNAMIC,NOTE ,GNU_STACK,GNU_RELRO共7个部分,其中PHDR保存程序头表;INTERP指定程序从可执行文件映射到内存后所必须调用的解释器;LOAD表示一个需要从二进制文件映射到虚拟地址空间的段,其中存储了常量数据、程序的目标代码等等;DYNAMIC存储了动态链接器所使用的信息;NOTE存储辅助信息;GNU_STACK是权限标志,用于标志栈是否为可执的;最后GNU_RELRO指定在重定位结束后判断哪些内存区域需要设置只读。
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello_objdump.s
图13 Ubuntu下链接重定位命令
5.5.1 分析hello与hello.o的不同之处:
不同之处:
- 链接增加了新的函数:在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
00000000004010d0 <exit@plt>:
4010d0: f3 0f 1e fa endbr64
4010d4: f2 ff 25 5d 2f 00 00 bnd jmpq *0x2f5d(%rip) # 404038 <exit@GLIBC_2.2.5>
4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010a0 <printf@plt>:
4010a0: f3 0f 1e fa endbr64
4010a4: f2 ff 25 75 2f 00 00 bnd jmpq *0x2f75(%rip) # 404020 <printf@GLIBC_2.2.5>
4010ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010e0 <sleep@plt>:
4010e0: f3 0f 1e fa endbr64
4010e4: f2 ff 25 55 2f 00 00 bnd jmpq *0x2f55(%rip) # 404040 <sleep@GLIBC_2.2.5>
4010eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010b0 <getchar@plt>:
4010b0: f3 0f 1e fa endbr64
4010b4: f2 ff 25 6d 2f 00 00 bnd jmpq *0x2f6d(%rip) # 404028 <getchar@GLIBC_2.2.5>
4010bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
2.链接增加了节:hello中增加了.init节和.plt节和一些节中定义的函数。
hello: 文件格式 elf64-x86-64
Disassembly of section .init:
0000000000401000 <_init>:
401000: f3 0f 1e fa endbr64
401004: 48 83 ec 08 sub $0x8,%rsp
401008: 48 8b 05 e9 2f 00 00 mov 0x2fe9(%rip),%rax # 403ff8
<__gmon_start__>
40100f: 48 85 c0 test %rax,%rax
401012: 74 02 je 401016 <_init+0x16>
401014: ff d0 callq *%rax
401016: 48 83 c4 08 add $0x8,%rsp
40101a: c3 retq
Disassembly of section .plt:
0000000000401020 <.plt>:
401020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
401026: f2 ff 25 e3 2f 00 00 bnd jmpq *0x2fe3(%rip) # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
40102d: 0f 1f 00 nopl (%rax)
401030: f3 0f 1e fa endbr64
401034: 68 00 00 00 00 pushq $0x0
401039: f2 e9 e1 ff ff ff bnd jmpq 401020 <.plt>
40103f: 90 nop
401040: f3 0f 1e fa endbr64
401044: 68 01 00 00 00 pushq $0x1
401049: f2 e9 d1 ff ff ff bnd jmpq 401020 <.plt>
40104f: 90 nop
401050: f3 0f 1e fa endbr64
401054: 68 02 00 00 00 pushq $0x2
401059: f2 e9 c1 ff ff ff bnd jmpq 401020 <.plt>
40105f: 90 nop
401060: f3 0f 1e fa endbr64
401064: 68 03 00 00 00 pushq $0x3
401069: f2 e9 b1 ff ff ff bnd jmpq 401020 <.plt>
40106f: 90 nop
401070: f3 0f 1e fa endbr64
401074: 68 04 00 00 00 pushq $0x4
401079: f2 e9 a1 ff ff ff bnd jmpq 401020 <.plt>
40107f: 90 nop
401080: f3 0f 1e fa endbr64
401084: 68 05 00 00 00 pushq $0x5
401089: f2 e9 91 ff ff ff bnd jmpq 401020 <.plt>
40108f: 90 nop
3.函数调用:hello中没有hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存空间地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,所以在.rela.text节中为其添加了重定位条目。
4.地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不甚明确的,其地址也是在运行时才确定的,因此访问也需要重定位:在汇编成机器语言时,将操作数全部设置为0,并且添加重定位条目。
5.5.2 链接的过程:
我通过分析hello和hello.o的不同之处,得到链接的主要过程就是链接就是链接器(ld)将各个目标文件(即各种.o文件)整合组装在一起时其各个目标文件中的各个库函数段会按照一定的规则累积在一起。
5.6 hello的执行流程
我通过edb的调试记录下每列函数调用call命令进入的函数如下所示:
图14 Ubuntu下edb运行程序
这其中的子函数名和地址(后6位)如下所示:
<_init> 401000
<.plt> 401020
<puts@plt> 401090
<printf@plt> 4010a0
<getchar@plt>4010b0
<atoi@plt> 4010c0
<exit@plt> 4010d0
<sleep@plt> 4010e0
<_start> 4010f0
<_dl_relocate_static_pie> 401120
<main> 401125
<__libc_csu_init> 4011c0
<__libc_csu_fini> 401230
<_fini> 401238
5.7 Hello的动态链接分析
在elf文件我寻找到:
[20] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
以及:
0x0000000000000003 (PLTGOT) 0x404000
之后便在Ubuntu系统下的edb中可以查看到:
让程序开始运行,点击上面的Run,当跑到完init部分时便可以发现Data Dump表的地址内容已经发生了变化:
对于各个变量而言,我们利用代码段和数据段的相对位置不变原则可以计算得出它们的正确地址。对于库函数而言,需要用plt、got共同考虑来合作:plt初始存储的是一批代码,把它们跳转到got所指示的位置,然后再调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的便是正确的内存地址。plt便可以跳转到正确区域。
图15 edb执行init之前的地址窗口
图16 edb执行init之后的地址窗口
5.8 本章小结
本章我主要是重新认真回顾了以往实验中在Linux系统中进行文件链接的过程。同时通过查看hello的虚拟地址空间,并且对比hello与hello.o的反汇编代码,我更充分地掌握了链接与重定位过程,了解到hello会在运行时要求动态链接器加载和链接到某个共享库,从而无需在编译时将那些库重新再链接到应用中,这为程序编写以及利用动态链接进行版本管理提供了一定的便利。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程是执行中程序的抽象,进程不只是程序代码,还包括当前活动,例如PC、寄存器的内容、堆栈、数据段,还可能包含堆等等。
进程的作用:
一个系统在同一时间好像同时运行多个程序,这是通过进程指令的交错执行实现的。每次运行程序时,Shell创建一个新进程,在该进程的上下文切换中运行该可执行目标文件。应用程序也可以创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。进程提供给应用程序关键抽象:一个独立的逻辑控制流如同程序独占处理器,一个私有的地址空间如同程序独占内存系统。
6.2 简述壳Shell-bash的作用与处理流程
壳Shell-bash作用:
解释命令,连接用户和操作系统以及内核。
处理流程:
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将所有处理结果中用到的注释删除,并按照以下顺序实行命令的检查:
- 内建的命令;
- Shell函数(由用户自己定义的);
- 可执行的脚本文件(需要寻找文件和PATH路径);
11.在执行前的最后一步是初始化所有的输入、输出重定向;
12.最后执行命令。
6.3 Hello的fork进程创建过程
根据Shell的处理流程可以推断,在输入命令执行hello之后,父进程如果判断出不是内部指令,就会通过fork()函数来创建子进程。子进程与父进程十分的近似,并得到一份与父进程用户级虚拟空间相同且独立的副本(包括数据段、代码、共享库、堆以及用户栈)。父进程打开的文件,子进程也可读写。二者间最明显的区别是pid的不同:fork()函数只会被调用一次,但会返回两次。在父进程中,fork返回子进程的pid;在子进程中,fork返回0。
6.4 Hello的execve过程
execve()函数加载并运行可执行目标文件hello后会附带上列表argv和环境变量列表envp。该函数的作用是在当前进程的上下文中加载并运行一个新的程序。只有当出现错误,例如找不到目标文件hello时,execve才会返回到调用程序,这里与上面提到的一次调用两次返回的fork()函数有所不同。在execve()加载目标文件hello之后,它调用了启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下所示的原型:
int main(intargc , char **argv , char *envp);
结合虚拟内存和内存映射过程,我可以更详细地说明exceve()函数实际上是如何加载和执行程序hello:
- 删除已存在的用户区域(自父进程独立);
- 映射私有区:为hello的代码、数据、.bss以及栈区域创建新的区域结构,所有这些区域都是私有的,编写时才可以复制;
- 映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到hello的,然后存储到用户虚拟地址空间中的共享区域内;
- 设置PC:exceve()的最后一项任务便是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
6.5.1 逻辑控制流:
一系列程序计数器 PC 的值的序列称为逻辑控制流。由于进程是轮流使用处理器的,同一个处理器在每个进程执行它自身流的一部分之后就被抢占了,紧接着便轮到其他的进程。
6.5.2 用户模式和内核模式:
处理器使用一个寄存器提供两种模式。其区别是:用户模式的进程既不允许执行特殊指令,也不允许直接引用地址空间中内核区的代码和数据;内核模式的进程既可以执行指令集中的任何命令,还可以访问系统中的任何内存位置。
6.5.3 上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值所构成。如下所示例如sleep进程的调度过程为:
图17 进程上下文切换
初始时,控制流在hello内,处于用户模式;
调用系统函数sleep()后,便进入内核模式,所以此时间片停止;
2s后,发送中断信号,再次转回用户模式,继续执行该指令。
6.5.4 调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并且可以重新开始一个先前被抢占了的进程,这种决策便称为调度。调度是由内核中的调度器代码处理的。当内核选择一个新进程运行时,即内核调度了这个进程。在内核调度一个新的进程运行之后,它就可以称之为抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。例如执行sleep()函数,sleep()函数请求调用休眠进程,sleep()将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。
6.5.5 用户模式与核心模式的相互转换:
为了能让处理器安全运行,从而不至于损坏操作系统,必须提前已知应用程序可执行指令所能访问的地址空间范围。因此就存在用户模式与核心模式的划分:核心模式可以戏称为“创世模式”,因为它拥有最高访问权限,此时处理器有以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,从而保证了系统的安全性。
6.6 hello的异常与信号处理
hello信号的正常运行状态如下所示:
图18 Ubuntu下执行hello程序的正常运行状态
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
异常类型:
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
处理方式:
图19 中断处理方式
图20 陷阱处理方式
图21 故障处理方式
图22 终止处理方式
中途不停乱按:只是将屏幕的输入缓存到缓冲区,我乱按敲出的乱码被认为是命令。
图23 Ubuntu下中途乱按运行状态
按Ctrl+Z:进程收到 SIGSTP 信号,hello进程挂起。此时可以用ps查看其进程pid,可以发现hello的pid是6655;再用jobs查看此时hello的后台 job号是1,可以通过调用 fg 1将其调回前台。
图24 Ubuntu下按Ctrl+Z运行状态
按Ctrl+C:进程收到 SIGINT 信号,结束hello。在ps中查询不到其pid,在job中也没有显示,所以可以看出hello已经被彻底结束。
图25 Ubuntu下按Ctrl+C运行状态
kill命令:挂起的进程被终止,在ps中已经无法查到到其PID。
图26 Ubuntu下输入kill命令运行状态
pstree命令:列出进程树可以找到hello的分支。
图27 Ubuntu下输入pstree命令运行状态
图28 Ubuntu下输入kill命令后再输入pstree命令运行状态
6.7本章小结
本章主要是介绍hello进程的执行过程。阐述了hello的创建、加载、终止以及通过键盘输入。程序是指令、数据及其组织形式的描述,进程是程序的实体。可以说进程就是运行的程序。在hello运行过程中,内核有选择地对其进行管理,决定何时进行上下文切换。也同样是在hello的运行过程中,当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对于不同的异常信号hello也有不同的处理结果。除此之外我还通过在百度等各类引擎上了解到进程是一个执行中程序的实例(Instance),它是计算机科学中最深刻、最成功的概念之一。即使操作系统中同时有多个程序执行,我们看到的也像是操作系统仅在运行前台程序一样,这是通过上下文切换实现的。操作系统根据某种特定的策略调度进程来在不同进程间快速有序地交错替换执行。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址的概念
逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分(hello.o)。
7.1.2 线性地址的概念
线性地址(Linear Address)是指逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址便生成了一个线性地址。
7.1.3 虚拟地址的概念
有时逻辑地址也称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也与实际物理内存容量无关,是hello中的虚拟地址。虚拟地址强调的是程序地址并非真实的物理地址,而是一个虚拟的地址(由逻辑地址表示),需要经过从线性地址再到物理地址的变换。
7.1.4 物理地址的概念
物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。物理地址对应的是计算机的主存,每个地址对应着主存的一个字节。早期计算机和数字信号处理器等系统使用物理地址直接进行寻址。若启用分页机制,则hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;若未启用分,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个Intel逻辑地址段式管理由段标识以及段内偏移量两部分组成。段标识符由一个16位长的字段组成,也可以称为段选择符,其中前13位是一个索引号。后3位包含一些硬件细节。
索引号可以通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
这里我只需关心Base字段,它描述了一个段的开始位置的线性地址。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
给定一个完整的逻辑地址段选择符+段内偏移地址,
判断段选择符的T1是0还是1,需要知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器得到其地址和大小。此时便得到一个数组了。
拿出段选择符中的前13位,可以在这个数组中,查找到对应的段描述符,Base它之后,即基地址就知道了。
Base + offset它之后,便得到转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是指一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理两部分。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后再把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,从而解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理,如下图所示:
图29 页式管理流程图
优点:
- 由于它不要求作业或进程的程序段和数据在内存中连续存放,从而它可以有效地解决了碎片问题;
- 动态页式管理提供了内存和外存统一管理的虚存实现方式,使得用户可利用的存储空间极大地增加,这既提高了主存的利用率,也有利于组织多道程序执行。
缺点:
1.要求有相应的硬件支持。例如地址变换机构,缺页中断的产生
选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本;
2.增加了系统开销,例如缺页中断处理机;
3.请求调页的算法如选择不当时,有可能产生“抖动”现象;
4.虽然消除了碎片,但是每个作业或进程的最后一页内总有一部分空间得不到利用。如果页面较大,那么这部分的损失仍然较大。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十甚至几百个周期。如果PTE碰巧缓存于L1中,那么开销就会下降1或2个周期。然而许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
多级(四级)页表:
将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目,如下图所示:
图30 使用k级页表进行翻译
多级页表常常用于减少常驻于内存中的页表大小。由于在同一时间并非所有虚拟内存都被分配,那么操作系统可以只记录那些已被分配的页来减小内存开销,这是通过多级页表实现的。对于以上的k(其中k>1)级页表,为了将虚拟地址转换为物理地址,MMU首先用第一段虚拟页号查询常驻于内存的一级页表,获取其二级页表的基址,再用第二段虚拟页号查询三级页表的基址,直到对第k级页表返回物理地址偏移,MMU就得到了该虚拟地址对应的物理地址。对于那些没有分配的虚拟地址,对应的多级页表根本不存在,只有当分配到它们时才会创建这些页表。因此多级页表可以减少内存需求。
解析VA,利用前m位VPN1来寻找一级页表位置,接着重复k次,直至在第k级页表获得了页表条目,最后将PPN与VPO组合便获得了PA。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照7.4的上述操作获得了物理地址PA。根据Cache组数的大小要求将PA分为CT(标记位)、CS(组号)以及CO(偏移量)。根据CS寻找到正确的组,比较每一个Cacheline的标记位是否有效以及CT是否相等,如果命中就直接返回想要的数据,如果未命中,就依次去L2、L3或者主存来判断是否命中:当命中时,将数据传给CPU同时更新各级Cache的Cacheline(如果Cache已满就采用换入换出策略)。最常见的三级Cache其主体虚拟结构如下图所示:
图31 三级Cache
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并且分配给它一个唯一的pid,同时为这个新进程创建虚拟内存。
它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并且将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中被返回时,新进程现有的虚拟内存刚好与调用fork时所存储的虚拟内存相同。当这两个进程中的任意一个后来进行写操作时,写时复制机制就会创建新页面。因此也为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
在bash中的进程中执行了如下的execve()函数调用:execve("hello",NULL,NULL);
execve()函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。
下面是加载并运行hello的几个步骤:
1.删除已存在的用户区域;
2.映射私有区域;
3.映射共享区域;
4.设置程序计数器(PC)。
exceve()函数最后设置当前进程的上下文中的程序计数器,指向代码区域的入口点。而下一次调度该进程时,他将从这个入口点开始执行。Linux系统将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
页面的正确命中完全是由硬件所完成的,而处理缺页故障是由硬件和操作系统内核所协作完成的,如下图缺页中断处理所示:
图32 缺页中断处理
其整体处理流程有如下步骤:
1.处理器生成一个虚拟地址,并将它传送给MMU;
2.MMU生成PTE地址,并从高速缓存/主存请求得到它;
3.高速缓存/主存向MMU返回PTE;
4.PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序;
5.缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改,那么就把它换到磁盘上;
6.缺页处理程序页面调入新的页面,并更新内存中的PTE;
7.缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以会正确命中。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配,要么是空闲。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有以下两种基本方法与策略:
7.9.1 带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
隐式空闲链表是指在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的,分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合,其中一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有放置策略例如首次适配以及下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
7.9.2 显示空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章我主要是介绍 hello 的存储器地址空间、 Intel的段式管理、 hello的页式管理,在指定环境下介绍了VA到PA的变换以及物理内存访问,除此之外还分别有hello进程fork时的内存映射、execve()函数调用时的内存映射、缺页故障与缺页中断处理以及动态存储分配管理。存储管理是操作系统中软硬件相结合的典型示例。虚拟内存提供了比可用内存更为广大的地址空间,为进程之间独立运行提供了可能,并且保障了内存的安全。高速缓存加速了程序的运行,使得程序员或者用户能够从更多的方面来优化程序。存储管理也是C/C++语言比其他语言更为灵活自由之处,需要我们深入学习来写出更加安全高效的程序。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.1.1 设备的模型化:文件
所有的I/O设备都被模型化为文件,甚至内核也被映射为文件。
8.1.2 设备管理:unix io接口
这种将设备优雅地映射为文件的方式,允许Linux系统内核引出一个简单、低级的应用接口,称为Unix I/O。
我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等等。
8.2 简述Unix IO接口及其函数
8.2.1 Unix IO接口:
1.打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作;
2.Linux shell创建的每个进程开始时都有三个已打开的文件:分别是标准输入(文件描述符0)、标准输出(描述符为1)以及标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值;
3.改变当前的文件位置:文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k;
4.读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;
5.写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k;
6.关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。
8.2.2 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;
char buf[256];
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;
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()函数将所有的参数内容格式化之后存储在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()函数的工作过程。除此之外我了解到Linux文件系统将许多概念都抽象成文件:网络、磁盘或者终端。这种抽象提供了一种对不同设备的统一处理方式。在调用Linux系统提供的接口时,我还注意到如read()和write()函数返回值的不足进而对程序做出正确处理。
(第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()函数,生成了子进程,再由execve()函数加载并且运行当前进程的上下文,在其中加载并运行hello新程序;
6.hello的变化过程中会出现各种地址,但是最终我真正所期待的是PA物理地址;
7.hello再运行时会调用一些函数,例如printf()函数,这些函数与Linux I/O的设备模拟化密切相关;
8.hello最终被shell父进程所回收,内核会收回为其创建的所有信息。
我经过大二下一整个学期通过CSAPP这本计算机基础书籍的顶级之作来对计算机系统进行细致学习,深刻认识到计算机系统绝对是经过无数先贤前人们呕心沥血的思考设计出的一个无比精妙且庞杂的整体。同时这门必修课使我理解了计算机隐藏在抽象外壳之下的一丝不苟的运作方式,这无时无刻不在指导自己如何写出效率更高、安全性更强的代码同时也逐渐养成了遇到计算机相关的问题时独立思考并勇于解决问题的良好学习习惯。我了解到计算机系统的基本概念,包括最底层的内存中的数据表示、流水线指令的构成、虚拟存储器、编译系统、动态加载库,以及用户应用等。除此之外著作中也提供了大量实际动手操作,可以帮助作为读者的我们更好地理解程序执行的方式,改进程序的执行效率。作为哈工大计算机专业的学生将来应该要成为优秀的程序员或计算机工程师而不是仅仅是底层的码农,基于实践在学习计算机,也不能忽略基于理论的重要性。最后我们也不能只盯着顶层的实现,而忽视了底层的构造,A programmer’s perspective正是对我们本科学习阶段的最好概况:远见、修养是更为重要的!
我想无论计算机在未来会产生怎样的变化与发展,这门必修课也不会失去其宝贵的价值。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件的作用 | 文件的名字 |
经过预处理后的hello源程序文本文件 | hello.i |
经过编译之后产生的汇编文件文本文件 | hello.s |
经过汇编之后产生的可重定位目标文件二进制文件 | hello.o |
通过链接产生的可执行目标文件二进制文件 | hello |
hello.o的ELF格式文本文件 | elf.txt |
hello.o的反汇编代码文本文件 | Disas_hello.s |
hello的ELF格式文本文件 | hello1.elf |
hello的反汇编代码文本文件 | hello1_objdump.s |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[8] 博客园 printf函数实现的深入剖析
[9] CSDN博客 ELF可重定位目标文件格式
[10] 博客园 shell命令执行过程
[11] [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)
(参考文献0分,缺失 -1分)