计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021110509
班 级 2103101
学 生 崔佳奇
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
摘 要
本文介绍了一个C程序Hello是如何在Linux系统下完成它一生的使命的。
Hello程序源于简简单单的Hello.c,在经过GCC下预处理、编译、汇编、链接这一串多重考验,最终成为了伟大的Hello可执行目标文件。在命令行输入./Hello按下回车那一刻,Hello便开始了在计算机系统的历程,计算机系统对其进行进程管理、信号处理、存储管理等一系列操作,最终Hello在系统进程中消失殆尽。这篇文章我会结合我所学的知识对Hello的一生进行深入探索。
关键词: 预处理;汇编;编译;链接;信号;进程
目 录
第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在Ubuntu下预处理的命令 - 6 -
2.3 Hello的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在Ubuntu下编译的命令 - 8 -
3.3 Hello的编译结果解析 - 8 -
3.4 本章小结 - 13 -
第4章 汇编 - 14 -
4.1 汇编的概念与作用 - 14 -
4.2 在Ubuntu下汇编的命令 - 14 -
4.3 可重定位目标elf格式 - 14 -
4.4 Hello.o的结果解析 - 17 -
4.5 本章小结 - 20 -
第5章 链接 - 21 -
5.1 链接的概念与作用 - 21 -
5.2 在Ubuntu下链接的命令 - 21 -
5.3 可执行目标文件hello的格式 - 23 -
5.4 hello的虚拟地址空间 - 26 -
5.5 链接的重定位过程分析 - 26 -
5.6 hello的执行流程 - 27 -
5.7 Hello的动态链接分析 - 28 -
5.8 本章小结 - 28 -
第6章 hello进程管理 - 29 -
6.1 进程的概念与作用 - 29 -
6.2 简述壳Shell-bash的作用与处理流程 - 29 -
6.3 Hello的fork进程创建过程 - 29 -
6.4 Hello的execve过程 - 29 -
6.5 Hello的进程执行 - 30 -
6.6 hello的异常与信号处理 - 31 -
6.7本章小结 - 34 -
第7章 hello的存储管理 - 35 -
7.1 hello的存储器地址空间 - 35 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 35 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 36 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 38 -
7.5 三级Cache支持下的物理内存访问 - 38 -
7.6 hello进程fork时的内存映射 - 39 -
7.7 hello进程execve时的内存映射 - 39 -
7.8 缺页故障与缺页中断处理 - 40 -
7.9动态存储分配管理 - 40 -
7.10本章小结 - 41 -
第8章 hello的IO管理 - 43 -
8.1 Linux的IO设备管理方法 - 43 -
8.2 简述Unix IO接口及其函数 - 43 -
8.3 printf的实现分析 - 43 -
8.4 getchar的实现分析 - 44 -
8.5本章小结 - 45 -
结论 - 46 -
附件 - 47 -
参考文献 - 48 -
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
Hello的P2P(From Program to Process)
Hello的源程序是hello.c,hello.c在经过预处理器cpp后,得到了修改后的源程序hello.i,经过编译器cc1后得到了汇编文件hello.s,经过汇编器as后得到了可重定位目标文件hello.o,最后,经过链接器ld得到了可执行目标文件hello。执行程序 “hello”,shell就会通过fork()创建一个子进程,子进程执行execve()在子进程的上下文中加载并执行hello。到这里P2P告一段落。
Hello的O2O(From Zero-0 to Zero-0)
shell在调用完execve()执行hello程序之后,系统内核就会为hello进程映射一份虚拟内存。在hello进入程序入口之后,hello的相关数据就会被系统内核加载到物理内存中,至此hello便开始了他的时间——hello正式被系统开始运行。为了保证正常执行,系统内核还要为hello做一系列工作,比如分配时间片、分配逻辑控制流等。hello运行结束后,它便成为了zombie进程,之后shell会负责回收它,最后,shell删除了一切和hello相关的内容。
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
使用的软件环境:VMware16虚拟机 Ubuntu22.04.01 LTS
硬件环境:AMD Ryzen7 5800H处理器 16 G RAM 512 G HD Disk
开发工具:gcc,edb,gdb
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 |
作用 |
hello.c |
Hello的源文件 |
hello.i |
Hello的修改了的源程序 |
hello.s |
Hello的汇编文件 |
hello.o |
Hello的可重定位目标文件 |
hello |
Hello的可执行目标文件 |
elfheader.txt |
hello.o的ELF头 |
secheader.txt |
hello.o的节头部表 |
relahello.txt |
hello.o的重定位节 |
hellosymbol.txt |
hello.o的符号表 |
disassembly.txt |
hello.o的反汇编 |
helloelfhead.txt |
hello的ELF 头 |
hellosection.txt |
hello的节头部表 |
hellodiss.txt |
hello的反汇编 |
hellorela.txt |
hello的重定位节 |
hellosym.txt |
hello的符号表 |
tree.txt |
进程树 |
表1 文件的中间结果
这一章主要介绍了一下shell执行hello的全过程,简单说明了hello从hello.c变成hello的全过程,说明了进行实验的软硬件环境和测试工具。
预处理的概念:
预处理就是通过预处理器cpp操作hello.c,根据hello.c中所有以 “#”开头的命令,比如一些宏定义、条件编译、include<.h>等,修改原始的C程序,并将引用的所有库展开合并成为一个完整的文本文件。
预处理的作用:
预处理可以是编写的程序更便于阅读、修改、移植和调回,同时还有利于模块化程序设计。
比如hello.c变成hello.i的过程,cpp把#include
gcc预处理的命令 Linux>gcc hello.c -E -o hello.i
图2.1 利用gcc对hello.c进行预处理
hello.i的图标
图2.2 产生的hello.i
hello.i的内容
图2.3 hello.i中的代码部分
预处理过后生成的hello.i文件内的内容相比于hello.c多了不少,但是其中的代码内容被完整的保留了下来。hello.i中包含了很多宏展开,头文件中的内容。
这一章介绍了C程序预处理的相关概念和作用,展现了Ubuntu下利用gcc预处理C文件的过程,并分析了.c和.i文件内容上的异同。
编译的概念:
简单的来说,编译就是将预处理文件(hello.i)生成汇编文件(hello.s)的过程。编译器cc1对hello.i经过一系列的语法分析、语义分析,并根据优化等级(-O)进行优化之后生成了相应的汇编代码文件,汇编代码文件是一个文本文件。
编译的作用:
编译可以将源程序(高级语言C语言)翻译成为优化后的汇编代码(低级语言),在这个阶段还可以对源程序进行语法检查、调试、修改、覆盖、优化等操作,以及一些嵌入式汇编的处理。
gcc编译的命令 Linux>gcc -S hello.i -o hello.s
图3.1 利用gcc对hello.i进行编译
3.3.1 数据
1. 字符串常量
在汇编文件标注.rodata这个位置上写着printf()中的字符串常量以及格式串
图3.2 .rodata节的字符串
查看main函数的汇编代码,按照x86-64的参数传递规则,argc应该存放在寄存器%edi中,argv应该存放在寄存器%rsi中。
程序的最开始就是把%rbp压入栈中,为main函数创建栈帧。
图3.3 main()函数的汇编代码
在调用printf之前寄存器%rdi和%rsi有所更新
图3.4 参数传递的汇编代码
从汇编代码来看恰好是argv中的元素传入了printf
3.3.2 全局函数
图3.5 全局函数的在汇编代码中的标志
从汇编代码的声明中可以看出 int main(int argc, char **argv)是被定义成为了全局函数,main中的字符串常量被存在.rodata节
3.3.3 赋值操作
在hello.c程序中,主要的赋值操作就是在for循环之前的i = 0,这一条指令,是利用mov指令来实现的
图3.6 赋值操作的汇编代码
此外程序中还有通过lea指令来实现赋值操作,lea指令是用来进行地址传送的,它只计算地址的值
图3.7 赋值操作的汇编代码
3.3.4 关系操作
(1)hello程序中,有一处条件判断指令:argc != 4,这个条件分支用于判断程序是否需要分区,对应的汇编代码是这个
图3.8 分支跳转比较的汇编代码
图3.9 分支跳转比较的汇编代码
这条代码的意思就是去比较8和-4(%rbp)中的值,设置对应的条件码,jle来根据条件码确定是否需要进行循环跳转,jle的条件码判断是(ZF ^ OF) | ZF,就是如果满足小于或等于就进行跳转
3.3.5 算数操作
(1)在有关栈的操作的时候,通常都会利用sub指令来对%rsp中的栈指针进行加减8(或者是8的整数倍)操作,对应的汇编代码有
图3.10 栈指针的减法操作
图3.11 参数的加一操作
3.3.6 控制转移指令
在汇编代码中,分支语句if - else以及各种循环都是通过条件跳转来实现的,对应的汇编代码有
图3.12 条件分支跳转的操作
3.3.7 类型转换
argv存放着参数字符串,但是sleep函数的参数类型是int型的,直接把参数传入是不可以的,所以程序调用了标准库中的函数atoi(),把字符串数字转换成int型的变量再传递给sleep使用,在汇编代码中就是call调用了atoi()函数进行类型转换的操作
图3.13 利用call调用atoi进行类型转换
3.3.8 函数操作
汇编代码中的函数调用通常都是使用call指令 + 函数名,在调用函数之前,汇编代码都会更新程序计数器%rip的值,确定函数调用结束后应该执行的指令。
在x86-64架构下,函数调用的时候前6个参数依次存放在%rdi,%rsi,%rdx,%rcx,%r8,%r9这6个通用寄存器中,其余的参数存放在栈。
函数的局部变量存放在栈中或者寄存器中。
函数的返回值存放在寄存器%rax中。
图3.14 利用call进行函数调用
函数调用结束后,会使用ret指令来结束调用
本章介绍了在编译过程中C语言转换成汇编代码的过程,同时练习了阅读汇编代码,明白了C语言和汇编代码之间指令的相关联系,知道了一些汇编指令的微操作。
汇编的概念:
汇编就是指汇编器as将汇编文件.s翻译成为机器语言,生成可重定位目标文件.o的过程,可重定位目标文件是二进制文件
汇编的作用:
汇编阶段可以生成可重定位目标文件,为下一步与其他程序相链接,和动态库,静态库相链接做好准备。
gcc汇编的命令 Linux>gcc hello.s -c -o hello.o
图4.1 利用gcc汇编生成hello.o
生成的hello.o文件
图4.2 生成的hello.o文件
查看指令 Linux> readelf -h hello.o > elfheader.txt
图4.3 hello.o文件的ELF头
ELF Header 文件包含了文件类别,数据类型,系统信息,链接器语法分析,程序入口地址等内容。
此外在文件靠前位置的Magic段主要是程序用来确认读入的是否是elf文件头,每次程序在读取elf头文件的时候,都会确认Magic是否正确,以防读入的不是elf文件。
查看指令 Linux> readelf -S hello.o > secheader.txt
节头部表包含了hello.o文件中各个节的类型、地址、大小、偏移量等信息
相关内容
There are 14 section headers, starting at offset 0x428: 节头: [号] 名称 类型 地址 偏移量 大小 全体大小 旗标 链接 信息 对齐 [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000098 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 000002d8 00000000000000c0 0000000000000018 I 11 1 8 [ 3] .data PROGBITS 0000000000000000 000000d8 0000000000000000 0000000000000000 WA 0 0 1 [ 4] .bss NOBITS 0000000000000000 000000d8 0000000000000000 0000000000000000 WA 0 0 1 [ 5] .rodata PROGBITS 0000000000000000 000000d8 000000000000003a 0000000000000000 A 0 0 8 [ 6] .comment PROGBITS 0000000000000000 00000112 000000000000002c 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 0000013e 0000000000000000 0000000000000000 0 0 1 [ 8] .note.gnu.pr[...] 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 00000398 0000000000000018 0000000000000018 I 11 9 8 [11] .symtab SYMTAB 0000000000000000 00000198 0000000000000108 0000000000000018 12 4 8 [12] .strtab STRTAB 0000000000000000 000002a0 0000000000000032 0000000000000000 0 0 1 [13] .shstrtab STRTAB 0000000000000000 000003b0 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), D (mbind), l (large), p (processor specific) |
表2 hello.o的节头部表
可以看到其属性分别有名称、类型、地址(没有重定位,这些都是0)、偏移量(节相对于文件开始的偏移)、节大小、全体大小、旗标(节属性)、链接(相对于其他节)、信息(附加节)、对齐(2的align次方的偏移)。
查看指令 Linux> readelf -r hello.o > relahello.txt
重定位节包含了各个段引用的外部符号等必要有效信息。在链接时需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断通过偏移量等信息应使用什么方法计算出正确地址值。
相关内容
图4.4 hello.o的重定位节
从中可以得到hello需要重定位的信息有:函数puts,exit,printf,atoi,sleep,getchar和rodata节中的两个字符串
查看指令 Linux> readelf -s hello.o > hellosymbol.txt
符号表存放着hello程序中定义和引用的全局变量和函数的相关信息,其中:name代表的符号的名称,value是这个符号相对于目标节的起始地址的偏移量,在这里还没有重定位所以都是0,size是对应目标的大小,type代表着数据类型(包含:无类型、节、函数),bind则指明这个符号是本地/局部符号还是全局符号
图4.5 hello.o的符号表
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
查看指令 Linux> objdump -d -r hello.o > disassembly.txt
hello.o的反汇编 |
hello.c的汇编 |
hello.o: 文件格式 elf64-x86-64 Disassembly of section .text: 0000000000000000 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 19 je 32 19: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 20 1c: R_X86_64_PC32 .rodata-0x4 20: 48 89 c7 mov %rax,%rdi 23: e8 00 00 00 00 call 28 24: R_X86_64_PLT32 puts-0x4 28: bf 01 00 00 00 mov $0x1,%edi 2d: e8 00 00 00 00 call 32 2e: R_X86_64_PLT32 exit-0x4 32: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 39: eb 4b jmp 86 3b: 48 8b 45 e0 mov -0x20(%rbp),%rax 3f: 48 83 c0 10 add $0x10,%rax 43: 48 8b 10 mov (%rax),%rdx 46: 48 8b 45 e0 mov -0x20(%rbp),%rax 4a: 48 83 c0 08 add $0x8,%rax 4e: 48 8b 00 mov (%rax),%rax 51: 48 89 c6 mov %rax,%rsi 54: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 5b 57: R_X86_64_PC32 .rodata+0x29 5b: 48 89 c7 mov %rax,%rdi 5e: b8 00 00 00 00 mov $0x0,%eax 63: e8 00 00 00 00 call 68 64: R_X86_64_PLT32 printf-0x4 68: 48 8b 45 e0 mov -0x20(%rbp),%rax 6c: 48 83 c0 18 add $0x18,%rax 70: 48 8b 00 mov (%rax),%rax 73: 48 89 c7 mov %rax,%rdi 76: e8 00 00 00 00 call 7b 77: R_X86_64_PLT32 atoi-0x4 7b: 89 c7 mov %eax,%edi 7d: e8 00 00 00 00 call 82 7e: R_X86_64_PLT32 sleep-0x4 82: 83 45 fc 01 addl $0x1,-0x4(%rbp) 86: 83 7d fc 08 cmpl $0x8,-0x4(%rbp) 8a: 7e af jle 3b 8c: e8 00 00 00 00 call 91 8d: R_X86_64_PLT32 getchar-0x4 91: b8 00 00 00 00 mov $0x0,%eax 96: c9 leave 97: c3 ret |
.file "hello.c" .text .section .rodata .align 8 .LC0: .string "\347\224\250\346\263\225: Hello 2021110509 \345\264\224\344\275\263\345\245\207 \347\247\222\346\225\260\357\274\201" .LC1: .string "Hello %s %s\n" .text .globl main .type main, @function 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), %rax movq %rax, %rdi call puts@PLT movl $1, %edi call exit@PLT .L2: movl $0, -4(%rbp) jmp .L3 .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), %rax movq %rax, %rdi movl $0, %eax call printf@PLT movq -32(%rbp), %rax addq $24, %rax movq (%rax), %rax movq %rax, %rdi call atoi@PLT movl %eax, %edi call sleep@PLT addl $1, -4(%rbp) .L3: cmpl $8, -4(%rbp) jle .L4 call getchar@PLT movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE6: .size main, .-main .ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4: |
表3 两种汇编的比较
在计算机中,机器语言是由一系列的0、1代码构成的,在反汇编文件中可以看到机器代码由16进制数来描述
每一个机器指令序列都包含的操作码、操作数等信息,以此一一对应着每一种汇编指令,把汇编转换成为机器语言
首先,机器语言中使用的操作数均为16进制表示,原始的汇编语言中的操作数都是10进制表示的。此外,编译阶段没有保留符号的名字,所以函数调用均写成主函数 + 偏移量的形式。
汇编代码hello.s中函数调用的call指令使用的是函数名称,反汇编代码中的call指令使用的是main函数相对偏移地址。由于函数只有在链接后才能确定所运行执行的地址(可以看到call指令对应机器代码的后四个字节均为0),所以为其添加重定位条目。
对机器语言而言,给定文件开始位置就能够将合法字节序列唯一地翻译解释成有效指令;但是机器反汇编代码的操作数却会被映射为特定字节或“小/大端序”来表示的十六进制立即数。
在这一章中我完成了汇编操作,将hello.s,转换成为了hello.o,并仔细的分析并阅读了可重定位目标文件的ELF内的相关内容,如ELF头、节头部表、符号表以及课程定位节。同时完成了对hello.o的反汇编操作,明白了汇编操作的原理和作用,分析比较了hello.s和反汇编文件之间的异同,明白了机器指令和汇编代码之间的一一对应关系,同时了解了之后链接对于一个C程序的作用。
第5章 链接
链接的概念:
链接就是指把各种代码和数据片段收集并组合成为一个文件的过程,这个文件就叫做可执行目标文件,它可以直接被加载到内存中执行。链接分为三种:
编译时链接(链接静态库,也称静态链接),加载时链接和运行时链接(动态链接,用于链接动态库)。
链接的作用:
可以使分块编程成为可能,有了链接,程序员不用去编写巨大的程序,同时也方便了程序的维护,更新和优化。
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
链接的指令 Linux> 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
图5.1 利用gcc链接
生成的可执行目标文件的图标
图5.2 生成的可执行目标文件
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.3.1 可执行目标文件的ELF Header
查看指令 Linux> readelf -a hello > helloelfhead.txt
图5.3 生成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 类型: EXEC (可执行文件) 系统架构: Advanced Micro Devices X86-64 版本: 0x1 入口点地址: 0x4010f0 程序头起点: 64 (bytes into file) Start of section headers: 13560 (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 |
表4 节头部表的内容
可以看到,相比于hello.o的ELF头,文件类型改变了,从REL变成了EXEC,入口点地址从0x0变成了0x4010f0(从不确定值变成了确定值),节头数量从14变成了27
5.3.2 可执行目标文件的Section Headers
利用指令 Linux> readelf -S hello > hellosection.txt
具体内容
There are 27 section headers, starting at offset 0x34f8: 节头: [号] 名称 类型 地址 偏移量 大小 全体大小 旗标 链接 信息 对齐 [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 00000000004002e0 000002e0 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.pr[...] NOTE 0000000000400300 00000300 0000000000000030 0000000000000000 A 0 0 8 [ 3] .note.ABI-tag NOTE 0000000000400330 00000330 0000000000000020 0000000000000000 A 0 0 4 [ 4] .hash HASH 0000000000400350 00000350 0000000000000038 0000000000000004 A 6 0 8 [ 5] .gnu.hash GNU_HASH 0000000000400388 00000388 000000000000001c 0000000000000000 A 6 0 8 [ 6] .dynsym DYNSYM 00000000004003a8 000003a8 00000000000000d8 0000000000000018 A 7 1 8 [ 7] .dynstr STRTAB 0000000000400480 00000480 0000000000000067 0000000000000000 A 0 0 1 [ 8] .gnu.version VERSYM 00000000004004e8 000004e8 0000000000000012 0000000000000002 A 6 0 2 [ 9] .gnu.version_r VERNEED 0000000000400500 00000500 0000000000000030 0000000000000000 A 7 1 8 [10] .rela.dyn RELA 0000000000400530 00000530 0000000000000030 0000000000000018 A 6 0 8 [11] .rela.plt RELA 0000000000400560 00000560 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 00000000000000cd 0000000000000000 AX 0 0 16 [16] .fini PROGBITS 00000000004011c0 000011c0 000000000000000d 0000000000000000 AX 0 0 4 [17] .rodata PROGBITS 0000000000402000 00002000 0000000000000042 0000000000000000 A 0 0 8 [18] .eh_frame PROGBITS 0000000000402048 00002048 00000000000000a0 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 0000000000000270 0000000000000018 25 7 8 [25] .strtab STRTAB 0000000000000000 000032e8 000000000000012e 0000000000000000 0 0 1 [26] .shstrtab STRTAB 0000000000000000 00003416 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), D (mbind), l (large), p (processor specific) |
表5 节头部表的内容
这里和hello.o不同的地方是,在可执行目标文件中,每个节的地址不是0x0,而是变成了根据节自身的大小和对齐规则计算得出的偏移量。
5.3.3 可执行目标文件的重定位节.rela.txt
利用指令 Linux> readelf -r hello > hellorela.txt
具体内容
重定位节 '.rela.dyn' at offset 0x530 contains 2 entries: 偏移量 信息 类型 符号值 符号名称 + 加数 000000403ff0 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0 000000403ff8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 重定位节 '.rela.plt' at offset 0x560 contains 6 entries: 偏移量 信息 类型 符号值 符号名称 + 加数 000000404018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0 000000404020 000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0 000000404028 000400000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0 000000404030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 atoi@GLIBC_2.2.5 + 0 000000404038 000700000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0 000000404040 000800000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0 |
表6 重定位节的内容
可以看到,相比于hello.o的重定位节,hello的重定位节被分成了两部分,包含动态链接的重定位内容,同时每一个部分都有了准确的偏移量,因而所有的加数都变成了0。
5.3.4 可执行目标文件的符号表
利用指令 Linux> readelf -s hello > hellosym.txt
具体内容如下
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 _[...]@GLIBC_2.34 (2) 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (3) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND [...]@GLIBC_2.2.5 (3) 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND [...]@GLIBC_2.2.5 (3) 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5 (3) 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (3) 8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (3) Symbol table '.symtab' contains 26 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS crt1.o 2: 0000000000400330 32 OBJECT LOCAL DEFAULT 3 __abi_tag 3: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c 4: 0000000000000000 0 FILE LOCAL DEFAULT ABS 5: 0000000000403e50 0 OBJECT LOCAL DEFAULT 19 _DYNAMIC 6: 0000000000404000 0 OBJECT LOCAL DEFAULT 21 _GLOBAL_OFFSET_TABLE_ 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_mai[...] 8: 0000000000404048 0 NOTYPE WEAK DEFAULT 22 data_start 9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 10: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 _edata 11: 00000000004011c0 0 FUNC GLOBAL HIDDEN 16 _fini 12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 13: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 22 __data_start 14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@GLIBC_2.2.5 15: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 16: 0000000000402000 4 OBJECT GLOBAL DEFAULT 17 _IO_stdin_used 17: 0000000000404050 0 NOTYPE GLOBAL DEFAULT 22 _end 18: 0000000000401120 5 FUNC GLOBAL HIDDEN 15 _dl_relocate_sta[...] 19: 00000000004010f0 38 FUNC GLOBAL DEFAULT 15 _start 20: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 __bss_start 21: 0000000000401125 152 FUNC GLOBAL DEFAULT 15 main 22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5 23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 24: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 25: 0000000000401000 0 FUNC GLOBAL HIDDEN 12 _init |
表7 符号表的内容
这部分相比于hello.o多出了动态链接解析出来的符号。同时其他的符号也多了不少。
使用edb来分析一下hello,查看一下虚拟空间的位置在哪里
图5.4 虚拟地址空间开头
可以看到,hello的虚拟是从0x400000开始的,根据hello的节头部表,我们得知.interp的偏移量是0x0002e0,所以它的位置应该就在0x4002e0,查看一下
图5.5 .interp节的内容
确实是这样
同样的,还可以找到.rodata节的位置在0x402000
[17] .rodata PROGBITS 0000000000402000 00002000 0000000000000042 0000000000000000 A 0 0 8 |
图5.6 .rodata节的内容
看到了固定的字符串!
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
首先对hello进行反汇编
指令 Linux> objdump -d -r hello > hellodiss.txt
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__@Base> 40100f: 48 85 c0 test %rax,%rax 401012: 74 02 je 401016 <_init+0x16> 401014: ff d0 call *%rax 401016: 48 83 c4 08 add $0x8,%rsp 40101a: c3 ret Disassembly of section .plt: 0000000000401020 <.plt>: 401020: ff 35 e2 2f 00 00 push 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8> 401026: f2 ff 25 e3 2f 00 00 bnd jmp *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 push $0x0 401039: f2 e9 e1 ff ff ff bnd jmp 401020 <_init+0x20> 40103f: 90 nop 401040: f3 0f 1e fa endbr64 401044: 68 01 00 00 00 push $0x1 401049: f2 e9 d1 ff ff ff bnd jmp 401020 <_init+0x20> 40104f: 90 nop 401050: f3 0f 1e fa endbr64 401054: 68 02 00 00 00 push $0x2 401059: f2 e9 c1 ff ff ff bnd jmp 401020 <_init+0x20> 40105f: 90 nop 401060: f3 0f 1e fa endbr64 401064: 68 03 00 00 00 push $0x3 401069: f2 e9 b1 ff ff ff bnd jmp 401020 <_init+0x20> 40106f: 90 nop 401070: f3 0f 1e fa endbr64 401074: 68 04 00 00 00 push $0x4 401079: f2 e9 a1 ff ff ff bnd jmp 401020 <_init+0x20> 40107f: 90 nop 401080: f3 0f 1e fa endbr64 401084: 68 05 00 00 00 push $0x5 401089: f2 e9 91 ff ff ff bnd jmp 401020 <_init+0x20> 40108f: 90 nop Disassembly of section .plt.sec: 0000000000401090 401090: f3 0f 1e fa endbr64 401094: f2 ff 25 7d 2f 00 00 bnd jmp *0x2f7d(%rip) # 404018 40109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 00000000004010a0 4010a0: f3 0f 1e fa endbr64 4010a4: f2 ff 25 75 2f 00 00 bnd jmp *0x2f75(%rip) # 404020 4010ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 00000000004010b0 4010b0: f3 0f 1e fa endbr64 4010b4: f2 ff 25 6d 2f 00 00 bnd jmp *0x2f6d(%rip) # 404028 4010bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 00000000004010c0 4010c0: f3 0f 1e fa endbr64 4010c4: f2 ff 25 65 2f 00 00 bnd jmp *0x2f65(%rip) # 404030 4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 00000000004010d0 4010d0: f3 0f 1e fa endbr64 4010d4: f2 ff 25 5d 2f 00 00 bnd jmp *0x2f5d(%rip) # 404038 4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 00000000004010e0 4010e0: f3 0f 1e fa endbr64 4010e4: f2 ff 25 55 2f 00 00 bnd jmp *0x2f55(%rip) # 404040 4010eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) Disassembly of section .text: 00000000004010f0 <_start>: 4010f0: f3 0f 1e fa endbr64 4010f4: 31 ed xor %ebp,%ebp 4010f6: 49 89 d1 mov %rdx,%r9 4010f9: 5e pop %rsi 4010fa: 48 89 e2 mov %rsp,%rdx 4010fd: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 401101: 50 push %rax 401102: 54 push %rsp 401103: 45 31 c0 xor %r8d,%r8d 401106: 31 c9 xor %ecx,%ecx 401108: 48 c7 c7 25 11 40 00 mov $0x401125,%rdi 40110f: ff 15 db 2e 00 00 call *0x2edb(%rip) # 403ff0 <__libc_start_main@GLIBC_2.34> 401115: f4 hlt 401116: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 40111d: 00 00 00 0000000000401120 <_dl_relocate_static_pie>: 401120: f3 0f 1e fa endbr64 401124: c3 ret 0000000000401125 401125: f3 0f 1e fa endbr64 401129: 55 push %rbp 40112a: 48 89 e5 mov %rsp,%rbp 40112d: 48 83 ec 20 sub $0x20,%rsp 401131: 89 7d ec mov %edi,-0x14(%rbp) 401134: 48 89 75 e0 mov %rsi,-0x20(%rbp) 401138: 83 7d ec 04 cmpl $0x4,-0x14(%rbp) 40113c: 74 19 je 401157 40113e: 48 8d 05 c3 0e 00 00 lea 0xec3(%rip),%rax # 402008 <_IO_stdin_used+0x8> 401145: 48 89 c7 mov %rax,%rdi 401148: e8 43 ff ff ff call 401090 40114d: bf 01 00 00 00 mov $0x1,%edi 401152: e8 79 ff ff ff call 4010d0 401157: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 40115e: eb 4b jmp 4011ab 401160: 48 8b 45 e0 mov -0x20(%rbp),%rax 401164: 48 83 c0 10 add $0x10,%rax 401168: 48 8b 10 mov (%rax),%rdx 40116b: 48 8b 45 e0 mov -0x20(%rbp),%rax 40116f: 48 83 c0 08 add $0x8,%rax 401173: 48 8b 00 mov (%rax),%rax 401176: 48 89 c6 mov %rax,%rsi 401179: 48 8d 05 b5 0e 00 00 lea 0xeb5(%rip),%rax # 402035 <_IO_stdin_used+0x35> 401180: 48 89 c7 mov %rax,%rdi 401183: b8 00 00 00 00 mov $0x0,%eax 401188: e8 13 ff ff ff call 4010a0 40118d: 48 8b 45 e0 mov -0x20(%rbp),%rax 401191: 48 83 c0 18 add $0x18,%rax 401195: 48 8b 00 mov (%rax),%rax 401198: 48 89 c7 mov %rax,%rdi 40119b: e8 20 ff ff ff call 4010c0 4011a0: 89 c7 mov %eax,%edi 4011a2: e8 39 ff ff ff call 4010e0 4011a7: 83 45 fc 01 addl $0x1,-0x4(%rbp) 4011ab: 83 7d fc 08 cmpl $0x8,-0x4(%rbp) 4011af: 7e af jle 401160 4011b1: e8 fa fe ff ff call 4010b0 4011b6: b8 00 00 00 00 mov $0x0,%eax 4011bb: c9 leave 4011bc: c3 ret Disassembly of section .fini: 00000000004011c0 <_fini>: 4011c0: f3 0f 1e fa endbr64 4011c4: 48 83 ec 08 sub $0x8,%rsp 4011c8: 48 83 c4 08 add $0x8,%rsp 4011cc: c3 ret |
表8 反汇编的内容
分析:
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
利用edb执行hello
图5.7 利用edb执行hello
调用的子程序名和程序地址
子程序名 |
程序地址 |
_start |
0x4010f0 |
_init |
0x401000 |
main |
0x40112d |
puts@plt |
0x401090 |
exit@plt |
0x4010d0 |
_fini |
0x4011c0 |
sleep@plt |
0x4010e0 |
atoi@plt |
0x4010c0 |
printf@plt |
0x4010a0 |
getchar@plt |
0x4010b0 |
表9 子程序地址
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接采用了延迟加载的策略,只有在调用函数的时候才会进行符号的映射。动态链接通过使用偏移量表GOT和过程链表PLT的协同工作来实现。
GOT表中存放着函数的目标地址,PLT表则使用GOT中的地址来跳转到目标函数,在程序执行的过程中,dl_init负责修改PLT和GOT。
首先先找到GOT的地址
调用edb查看调用dl_init前后的变化
调用前
调用后
图5.8 调用前后地址的变化
这一章我介绍了hello.o的链接过程,分析比较了hello和hello.o的ELF文件的异同,仔细分析了动态链接过程中的各种变化,理解了链接的原理过程。
进程的概念:
经典的定义就是一个执行中程序的实例,每一个进程都有它自己的地址空间,一般情 况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数 据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动 过程调用的指令和本地变量。
进程的作用:
进程为用户提供了以下假象:
(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
图6.1 进程的逻辑控制流
Shell-bash的作用:
shell是一个交互型的应用程序,代表用户运行其他程序,执行一系列的读、求值步骤,然后终止。
Shell-bash的处理流程:
shell的基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid()(或wait…等待作业终止后返回。
7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
创建过程:
进程的创建采用fork()函数:pid_t fork();创建的子进程得到与父进程用户级虚拟地址空间相同的(但是物理空间是独立的,所以两个进程中的变量值是不相关联的)一份副本,包括代码和数据段、堆、共享库以及用户栈。
子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork()函数时,子进程可以读取父进程中打开的任何文件。
父进程与创建的子进程之间最大的区别在于它们有不同的PID。子进程中,fork()返回0。父进程中fork()会返回子进程的PID,因为子进程的PID总是为非零,返回值就提供一个明确的方法分辨程序是在父进程还是在子进程中。还可以进行多进程处理任务。
利用进程图就可以很形象的描述fork()的作用
图6.2 进程图示意
execve()函数在当前进程的上下文中加载并运行一个新程序。execve()函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。只有当出现错误时,例如:找不到文件名,调用发生错误,execve()才会返回到调用程序,调用成功不会返回。
与fork()不同,fork()一次调用两次返回,execve()一次调用从不返回(必须是成功调用)。
图6.3 用户栈的结构
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成(不包含代码段和数据段)。
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。上下文切换的时候,进程就处于内核模式。
图6.3 上下文切换
对于hello程序,程序在调用了sleep()后就会进入内核模式,并会进行上下文切换。程序运行到getchar()的时候,内核也会进行上下文切换,让其他进程运行。
除此之外,系统还会为hello程序分配时间片,只要hello时间片被用完,即使没有执行到getchar()或者sleep()函数,系统就会判断当前程序的运行时间已经足够久了,从而进行上下文切换,将处理器让给其他进程,这样可以提升程序运行的效率。
类别 |
原因 |
异步/同步 |
返回行为 |
中断 |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
故障 |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
终止 |
不可恢复的错误 |
同步 |
不会返回 |
图1 异常处理
图2 陷阱处理
图3 故障处理
图4 终止处理
1. 运行时键入ctrl + c,向hello发送SIGINT信号
图6.4 发送信号
图6.5 发送信号
此时输入ps ,监视后台程序
图6.5 进程监视
可以看到hello在后台被挂起了
输入jobs来查看当前被暂停的进程
图6.6 进程状态查看
输入pstree,查看进程树
systemd-+-ModemManager---2*[{ModemManager}] |-NetworkManager---2*[{NetworkManager}] |-VGAuthService |-accounts-daemon---2*[{accounts-daemon}] |-acpid |-avahi-daemon---avahi-daemon |-bluetoothd |-colord---2*[{colord}] |-cron |-cups-browsed---2*[{cups-browsed}] |-cupsd |-dbus-daemon |-gdm3-+-gdm-session-wor-+-gdm-wayland-ses-+-gnome-session-b---2*[{gnome-session-b}] | | | `-2*[{gdm-wayland-ses}] | | `-2*[{gdm-session-wor}] | `-2*[{gdm3}] |-gnome-keyring-d---3*[{gnome-keyring-d}] |-irqbalance---{irqbalance} |-2*[kerneloops] |-networkd-dispat |-packagekitd---2*[{packagekitd}] |-polkitd---2*[{polkitd}] |-power-profiles----2*[{power-profiles-}] |-rsyslogd---3*[{rsyslogd}] |-rtkit-daemon---2*[{rtkit-daemon}] |-snapd---13*[{snapd}] |-switcheroo-cont---2*[{switcheroo-cont}] |-systemd-+-(sd-pam) | |-at-spi2-registr---2*[{at-spi2-registr}] | |-2*[dbus-daemon] | |-dconf-service---2*[{dconf-service}] | |-evolution-addre---5*[{evolution-addre}] | |-evolution-calen---8*[{evolution-calen}] | |-evolution-sourc---3*[{evolution-sourc}] | |-fcitx---2*[{fcitx}] | |-fcitx-dbus-watc | |-gedit---3*[{gedit}] | |-2*[gjs---6*[{gjs}]] | |-gnome-session-b-+-at-spi-bus-laun-+-dbus-daemon | | | `-3*[{at-spi-bus-laun}] | | |-evolution-alarm---5*[{evolution-alarm}] | | |-gsd-disk-utilit---2*[{gsd-disk-utilit}] | | |-update-notifier---3*[{update-notifier}] | | `-3*[{gnome-session-b}] | |-gnome-session-c---{gnome-session-c} | |-gnome-shell-+-Xwayland | | |-gjs---7*[{gjs}] | | `-10*[{gnome-shell}] | |-gnome-shell-cal---5*[{gnome-shell-cal}] | |-gnome-terminal--+-3*[bash] | | |-bash-+-hello | | | `-pstree | | `-3*[{gnome-terminal-}] | |-goa-daemon---3*[{goa-daemon}] | |-goa-identity-se---2*[{goa-identity-se}] | |-gsd-a11y-settin---3*[{gsd-a11y-settin}] | |-gsd-color---3*[{gsd-color}] | |-gsd-datetime---3*[{gsd-datetime}] | |-gsd-housekeepin---3*[{gsd-housekeepin}] | |-gsd-keyboard---3*[{gsd-keyboard}] | |-gsd-media-keys---3*[{gsd-media-keys}] | |-gsd-power---3*[{gsd-power}] | |-gsd-print-notif---2*[{gsd-print-notif}] | |-gsd-printer---2*[{gsd-printer}] | |-gsd-rfkill---2*[{gsd-rfkill}] | |-gsd-screensaver---2*[{gsd-screensaver}] | |-gsd-sharing---3*[{gsd-sharing}] | |-gsd-smartcard---3*[{gsd-smartcard}] | |-gsd-sound---3*[{gsd-sound}] | |-gsd-wacom---3*[{gsd-wacom}] | |-gsd-xsettings---3*[{gsd-xsettings}] | |-gvfs-afc-volume---3*[{gvfs-afc-volume}] | |-gvfs-goa-volume---2*[{gvfs-goa-volume}] | |-gvfs-gphoto2-vo---2*[{gvfs-gphoto2-vo}] | |-gvfs-mtp-volume---2*[{gvfs-mtp-volume}] | |-gvfs-udisks2-vo---3*[{gvfs-udisks2-vo}] | |-gvfsd-+-gvfsd-dnssd---2*[{gvfsd-dnssd}] | | |-gvfsd-network---3*[{gvfsd-network}] | | |-gvfsd-smb-brows---4*[{gvfsd-smb-brows}] | | |-gvfsd-trash---2*[{gvfsd-trash}] | | `-2*[{gvfsd}] | |-gvfsd-fuse---5*[{gvfsd-fuse}] | |-gvfsd-metadata---2*[{gvfsd-metadata}] | |-nautilus---5*[{nautilus}] | |-pipewire---{pipewire} | |-pipewire-media----{pipewire-media-} | |-pulseaudio---3*[{pulseaudio}] | |-snap-store---4*[{snap-store}] | |-snapd-desktop-i---3*[{snapd-desktop-i}] | |-sogoupinyin-ser---9*[{sogoupinyin-ser}] | |-sogoupinyin-wat---5*[{sogoupinyin-wat}] | |-tracker-miner-f---5*[{tracker-miner-f}] | |-vmtoolsd---3*[{vmtoolsd}] |-systemd-journal |-systemd-logind |-systemd-oomd |-systemd-resolve |-systemd-timesyn---{systemd-timesyn} |-systemd-udevd |-udisksd---4*[{udisksd}] |-unattended-upgr---{unattended-upgr} |-upowerd---2*[{upowerd}] |-vmtoolsd---3*[{vmtoolsd}] |-vmware-vmblock----2*[{vmware-vmblock-}] `-wpa_supplicant |
表11 进程树
可以从进程树中看到hello这个进程被挂起了
输入fg,让hello重新到前台执行
图6.7 程序运行
运行kill指令,让hello终止运行
先查看一下hello的PID
图6.8 程序运行
输入 kill -9 3993就可以了
图6.9 杀死进程
再查看一下进程
图6.10 查看进程
hello已经被杀死了
图6.11 运行时按回车
并没有什么异常
图6.12 运行时乱按
可以看到随便乱按并不会影响到当前进程的运行,不仅如此,shell会依次记录你的输入的命令,在当前进程运行结束之后去依次处理的输入的内容。
这一章我详细介绍了进程的概念和作用,并仔细分析了fork()和execve()的调用方法,过程和作用,上下文切换的原理和过程,还有对于hello的信号异常的处理与分析。
第7章 hello的存储管理
各种地址空间的概念:
逻辑地址:逻辑地址指的是在汇编代码中通过偏移量+段基址得到的地址,所以汇编代码中所有显示的地址都是逻辑地址。
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。第一个字节的地址为0,写下来的字节地址为1,再下一个为2,以此类推。所以物理地址就像是一个大的数组。
在Intel下,对于cache的访问时通过物理地址来实现的,一个物理地址可以被分成三个部分:标记位,组索引,块偏移
图7.1 物理地址的构成
Intel i7芯片中的cache都是以组相联的方式构成的,也就是说cache被分成了很多块,块分成了很多组,组内有分成了很多行。在访问cache的时候,首先通过组索引来找到我们的地址在Cache中所对应的组号,再通过标记和Cache的有效位来判断我们的内容是否在Cache中。若命中则通过块偏移读取我们要的数据,若不命中则从下一级Cache(准确的来说是下一层存储空间,因为L3 cache不命中会从主存DRAM中寻找)中寻找。
讨论一个例子,这个是我的电脑L1 D-cache 的信息
图7.2 L1D-cache的信息
可以知道每一个L1 D-cache的大小为32KB,每一组cache中有8行,查询我的CPU的配置,可以知道每一块是64B,物理地址位数是52位,经过公式
C=S *E*B,可以知道我的电脑中组索引为6位,块偏移为6位,标志位40位。
这一章我简单介绍了计算机系统中的逻辑地址和物理地址这两个地址空间的概念,并且详细分析了物理地址的构成,含义,寻址方式,结合自己电脑的配置完成了对于cache寻址方式的了解。
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; } |
函数中的va_list就是typedef后的char*字符串。va_list arg = (va_list)((char*)(&fmt) + 4);就是得到了后面所有参数(就是...)中的第一个量。
之后printf调用vsprintf函数。vsprintf函数将我们需要输出的字符串格式化并把内容存放在buf(缓冲区)中。并返回要输出的字符个数i。然后调用系统函数write来在屏幕上打印buf中的前i个字符,也就是我们要输出的格式串。
调用write系统函数后,程序进入到陷阱,系统调用 int 0x80或syscall指令等,通过字符驱动子程序打印printf中的内容。
最后程序返回我们实际输出的字符数量i,也就是说printf是由返回值的,只是我们很少去使用。
查看一下getchar()函数的定义
int getchar (void) |
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII 码,保存到系统的键盘buf缓冲区之中。
getchar() 函数落实到底层调用了系统函数 read(),通过系统调用 read() 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar() 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
根据我的所学知识,这一章我总结介绍了printf()和getchar()这两个I/O函数的具体实现过程,理解了计算机系统是如何实现内部和外部相互联系并且同时运作的。
Hello最初只是一个简简单单的hello.c文件,只是一些简简单单的文本,它能够在屏幕上显示出 “Hello World!”,真的经历了许多考验。
一开始,hello.c只是默默的存在磁盘当中,终于有一刻,有一位程序员使用了gcc,让hello.c经过了预处理,其中的头文件被展开,宏常量被存入了hello.i中。接着,编译器cc1又对它进行了一番处理,hello.c变成了较为低级的存在——hello.s汇编代码,但是这远远不够,只有经历了编译器的考验,变成机器能够认识的语言——机器语言,才能够继续它的使命。俗话说,一木不成林,仅仅靠hello.o单个程序还是难以运行,只有经过了链接器的处理,有了动态库的合作,hello.c才最终成为了伟大的hello——可执行目标文件。
当在命令行中输入了./hello之后,“Hello World!”完美的出现在了屏幕上,这个过程还有这shell的功劳,shell解析了输入命令行的命令,调用fork()创建子进程,并用execve()在子进程的上下文中加载程序,经过一系列操作,hello终于进入了CPU的内部。
最后,hello运行完成,它完成了它伟大的使命,默默的消失在了茫茫的计算机系统中,被回收,再次回到了孤单的磁盘之中。
计算机系统,知识很多,难度很大,但是内容真的很有趣,我很喜欢,我一定会在未来继续深入探究。
列出所有的中间产物的文件名,并予以说明起作用。
文件名 |
作用 |
hello.c |
Hello的源文件 |
hello.i |
Hello的修改了的源程序 |
hello.s |
Hello的汇编文件 |
hello.o |
Hello的可重定位目标文件 |
hello |
Hello的可执行目标文件 |
elfheader.txt |
hello.o的ELF头 |
secheader.txt |
hello.o的节头部表 |
relahello.txt |
hello.o的重定位节 |
hellosymbol.txt |
hello.o的符号表 |
disassembly.txt |
hello.o的反汇编 |
helloelfhead.txt |
hello的ELF 头 |
hellosection.txt |
hello的节头部表 |
hellodiss.txt |
hello的反汇编 |
hellorela.txt |
hello的重定位节 |
hellosym.txt |
hello的符号表 |
tree.txt |
进程树 |
表12 中间文件