计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2022******
班 级 2203***
学 生 邵渤涵
指 导 教 师 史先俊、刘研、吴晋、高煜博、胡睿
计算机科学与技术学院
2023****年4月
摘 要
本文通过对hello程序的生命周期全过程的分析,回顾了计算机系统课程的知识点。在使用Ubuntu操作系统及相关工具的基础上,深入探讨了程序从编写到进程结束的各个阶段,揭示了Linux下一般程序的P2P和020过程,展现了x86-64计算机系统的主要工作机制,加深了对计算机系统工作与原理的理解。
**关键词:**计算机系统;生命周期分析;Linux操作系统;x86-64架构
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
**
**
目 录
6.2 简述壳Shell-bash的作用与处理流程… - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理… - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理… - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换… - 11 -
7.7 hello进程execve时的内存映射… - 11 -
**
**
第1章 概述
1.1 Hello简介
Hello的P2P(Program to Process)过程始于程序员编写hello的原始C语言代码,通过预处理、编译、汇编、链接等步骤,最终生成可执行文件。Shell负责为hello创建进程,并加载可执行文件,将其转化为运行中的进程。这个过程中,hello从代码状态逐步变为运行状态,涉及了程序与进程的转变。
Hello的020(Zero to Zero)过程在Shell为hello创建进程时发生。Shell调用fork函数生成新的进程,使用execve加载hello的可执行文件,为其提供虚拟地址空间等上下文,将hello从无到有地构建为一个运行中的进程。这一过程揭示了进程的创建、加载和初始化阶段。
在运行时,hello经历了异常与信号处理、对存储器的访问、中断和IO端口与外设的交互等机制。最终,hello进程可能正常退出或收到信号终止,操作系统结束并回收进程,返回Shell。这完整地展示了hello从运行到结束的过程,符合计算机系统中从零到零(from zero to zero)的生命周期。
1.2 环境与工具
硬件环境:处理器:Intel® Core™ i7-12700H CPU @ 4.70GHz RAM:16.00GB
系统:Ubuntu 22.04.3 LTS 64位,VMware® Workstation 17 Pro
软件环境:gcc,as,ld,vim,edb,readelf,gedit,gdb
1.3 中间结果
中间结果文件名称 | 文件作用 |
---|---|
hello | 可执行目标程序 |
hello.elf | hello的elf格式文件 |
hello.i | 预处理后的文件 |
hello.o | 可重定位目标文件 |
hello.s | 汇编程序 |
hello_o.elf | hello.o的elf格式文件 |
hello_o_obj.txt | hello.o的反汇编代码 |
hello_obj.txt | hello的反汇编代码 |
1.4 本章小结
本章概述了hello程序的执行流程,涵盖了P2P和020的过程。介绍了实验环境、工具及生成的中间结果。
(第1章0.5分)
**
**
第2章 预处理
2.1 预处理的概念与作用
预处理器根据以字符#开头的命令修改原始C程序,包括处理头文件(如#include<stdio.h>)、宏定义替换、条件编译(通过#ifdef确定执行的代码段)、处理特殊符号(如#error),以及删除源程序中的注释部分。这过程增强了代码的可读性、维护性,促使代码重用和条件编译等操作。
2.2在Ubuntu下预处理的命令
通过以上这条命令可以对hello.c进行预处理。其中指令 -E将执行预处理操作也即生成*.i文件,gcc编译器将对#开头的指令进行解析。然后使用指令-o将结果输出到hello.i文件当中。
2.3 Hello的预处理结果解析
最开始的这一部分是hello.c中所有需要涉及到的头文件信息。
随后是一些typedef的别名定义。
然后是在预处理器的作用下,被#include的头文件主体内容包括大量函数声明和少量结构体定义等。这些内容在预处理阶段完全经过宏展开,直接插入到程序文本中,为源代码提供了所需的函数和结构声明,为编译器提供了丰富的信息,进一步支持程序的正确编译和执行。
文件的最后是真正的hello.c的内容,该内容在预处理器阶段受到较少处理。
2.4 本章小结
在预处理过程中,预处理器的工作包括头文件展开、宏替换、删除注释、条件替换等。通过在Ubuntu系统中展示对hello.c
文件的预处理过程与结果,可以清晰地演示预处理器如何处理包含的头文件、替换宏定义、删除注释,并根据条件编译执行相应的代码段。这过程为理解C程序的编译过程提供了实际的示例和可视化。
(第2章0.5分)
**
**
第3章 编译
3.1 编译的概念与作用
概念上,编译器(ccl)将文本文件 hello.i 翻译成包含汇编语言程序的文本文件 hello.s。
编译的主要作用包括:
- 扫描(词法分析): 将源代码分割成符合语法要求的字符单元,可通过自上而下或自下而上分析。
- 语法分析: 基于词法分析生成语法分析树。
- 语义分析: 静态语义分析判断指令是否合法,不涉及执行时可能的错误。例如,不会检查动态类型检查时才能发现的错误。
- 中间代码: 产生更清晰的逻辑,为代码优化提供基础。
- 代码优化: 根据用户指定的优化等级进行安全、等价的代码优化,提升执行时的性能。
- 生成代码: 在最终阶段生成汇编语言代码文件 hello.s,该文件以汇编语言格式展示源代码。
3.2 在Ubuntu下编译的命令
通过以上这条命令可以对hello.i进行编译。执行-S(大写)指令将*.i文件中源码转化为汇编代码*.s文件。然后使用指令-o将结果输出到hello.s文件当中。
3.3 Hello的编译结果解析
3.3.1数据类型处理
-
常量和字符串处理
-
.section .rodata.str1.8,“aMS”,@progbits,1
-
.align 8
-
.LC0:
-
.string “\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201”
-
.section .rodata.str1.1,“aMS”,@progbits,1
-
.LC1:
-
.string “Hello %s %s\n”
在数据类型的处理中,字符串常量 .LC0 被分配在只读数据段(.rodata),并以UTF-8编码存储问候消息。另外,.LC1 包含了 printf 的格式字符串。
- 变量处理
- movq %rsi, %rbx
这一部分涉及到变量的处理。在这里,将 %rsi 中的值(局部变量)传递给 %rbx 寄存器,以便在后续的操作中使用。
- movq 16(%rbx), %rcx
- movq 8(%rbx), %rdx
- movq 24(%rbx), %rdi
这些指令用于将结构体中的成员值加载到寄存器中,对结构体的成员进行操作。
-
表达式和类型处理
-
movl $0, %ebp
-
movl $.LC0, %edi
-
call puts
-
movl $1, %edi
-
call exit
这一部分包含了表达式和类型的处理。通过 movl 指令,将0赋给 %ebp 寄存器,并调用 puts 函数。然后,将1传递给 %edi 寄存器,调用 exit 函数。
3.3.2操作处理
-
赋值和逗号操作符
-
movq %rsi, %rbx
-
movl $0, %ebp
-
movq 24(%rbx), %rdi
-
call strtol
-
movl %eax, %edi
在这里,movq 指令将 %rsi 寄存器中的值赋给 %rbx 寄存器。接着,通过 movl 指令将0赋给 %ebp 寄存器。最后,使用 call 指令调用 strtol 函数,将 %rdi 寄存器中的值转换为长整型,并将结果保存在 %eax 寄存器中。
- 赋初值/不赋初值
- int i;
在这段代码中,局部变量 i 被声明但没有明确的赋初值,相应的汇编指令不会显式出现。编译器会根据需要在栈上分配空间,但不一定会对其进行初始化。
-
类型转换
-
movq 24(%rbx), %rdi
-
movl $10, %edx
-
movl $0, %esi
-
call strtol
这里使用 strtol 函数进行字符串到长整型的转换。通过 movq 指令将 %rdi 寄存器中的字符串地址传递给 strtol 函数。同时,使用 movl 指令将10和0分别传递给 %edx 和 %esi 寄存器,作为额外的参数。
- 算术操作
- addl $1, %ebp
在这里,addl 指令对 %ebp 寄存器中的值进行加法操作。这模拟了一个简单的自增操作,即 ++i。
-
关系操作
-
cmpl $4, %edi
-
jne .L6
这里使用 cmpl 指令进行比较操作,判断 %edi 寄存器中的值是否不等于4。如果不等,则跳转到 .L6 标签。
-
数组/指针/结构操作
-
movq 16(%rbx), %rcx
-
movq 8(%rbx), %rdx
这些指令涉及到数组和结构体的成员操作。通过 movq 指令,将 %rbx 寄存器中的值的偏移量加到寄存器中,以获取数组和结构体成员的地址。
-
控制转移
-
jmp .L2
-
…
-
.L2:
-
…
-
jle .L3
控制转移部分使用了 jmp 和 jle 指令,实现了条件跳转和循环结构。
-
函数操作
-
movq stdin(%rip), %rdi
-
call getc
这里使用 call 指令调用了 getc 函数,涉及了函数调用和参数传递。
- movl $.LC0, %edi
- call puts
通过 movl 指令将字符串常量 .LC0 的地址传递给 %edi 寄存器,然后调用 puts 函数。
- movl $1, %edi
- call exit
将1传递给 %edi 寄存器,然后调用 exit 函数。
- movq 24(%rbx), %rdi
- movl $10, %edx
- movl $0, %esi
- call strtol
通过 movq 指令将 %rbx 寄存器中的字符串地址传递给 %rdi 寄存器,同时将10和0传递给 %edx 和 %esi 寄存器,然后调用 strtol 函数进行字符串到长整型的转换。
3.4 本章小结
通过详细解析汇编代码,我们深入了解了编译器如何处理C语言中的各种数据类型和操作。从字符串常量、变量处理、表达式、类型转换、赋值等方面,我们看到了汇编代码是如何映射到高级语言的不同概念的。此外,针对逻辑控制流、循环结构、函数调用等,我们也解析了相应的指令和操作,进一步理解了编译器是如何将高级语言翻译为底层机器代码的过程。这种分析有助于加深对编程语言底层执行机制的理解。
(第3章2分)
**
**
第4章 汇编
4.1 汇编的概念与作用
汇编语言是一种低级计算机编程语言,使用符号和助记符代替机器语言的二进制指令,使程序员能更容易理解和编写程序。汇编代码以.asm或.s文件存储,通过汇编器转换为目标文件(.o),其中包含机器语言代码和元数据。
作用:
- 底层控制: 汇编语言允许程序员直接操作计算机硬件,提供更细粒度的硬件控制,适用于性能敏感的场景,如嵌入式系统和操作系统内核。
- 调试: 相对于机器语言,汇编语言更易理解,使程序员能够在调试过程中轻松追踪代码执行和查看变量值。
- 汇编指令: 提供了与机器语言指令相对应的符号和助记符,简化了编程过程。
- 目标文件与链接: 汇编器将汇编代码转换为目标文件,链接器将目标文件组合成可执行文件,完成程序的最终生成。
4.2 在Ubuntu下汇编的命令
通过以上这条命令可以对hello.s进行编译。执行-c(小写)指令将*.s文件中的汇编源码转化未机器能执行的二进制机器码,生成文件*.o。
4.3 可重定位目标elf格式
使用readelf解析汇编器生成的可重定位目标文件hello.o,结果如下:
- ELF头:
- Magic: 文件标识,以16进制表示。
- 类别、数据、版本、OS/ABI: 文件的基本属性,如64位(ELF64)、小端序、UNIX - System V等。
- 类型: REL表示可重定位文件。
- 系统架构: 针对AMD的X86-64架构。
- 入口点地址、程序头大小、节头大小: 文件属性和布局相关信息。
- 节头数目: 共15个节。
- 重定位项目: 包含.rela.text和.rela.eh_frame两个重定位节。
- 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: 1152 (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: 15
- Section header string table index: 14
- 节头
- 每个节的编号、名称、类型、地址、偏移量、大小、全体大小等信息。
- 标志列解释了每个节的属性,如是否可写、是否可执行等。
- .rela.text和.rela.eh_frame是重定位节。
- 节头:
- [号] 名称 类型 地址 偏移量
- 大小 全体大小 旗标 链接 信息 对齐
- [ 0] NULL 0000000000000000 00000000
- 0000000000000000 0000000000000000 0 0 0
- [ 1] .text PROGBITS 0000000000000000 00000040
- 0000000000000083 0000000000000000 AX 0 0 1
- [ 2] .rela.text RELA 0000000000000000 00000300
- 00000000000000d8 0000000000000018 I 12 1 8
- [ 3] .data PROGBITS 0000000000000000 000000c3
- 0000000000000000 0000000000000000 WA 0 0 1
- [ 4] .bss NOBITS 0000000000000000 000000c3
- 0000000000000000 0000000000000000 WA 0 0 1
- [ 5] .rodata.str1.8 PROGBITS 0000000000000000 000000c8
- 0000000000000026 0000000000000001 AMS 0 0 8
- [ 6] .rodata.str1.1 PROGBITS 0000000000000000 000000ee
- 000000000000000d 0000000000000001 AMS 0 0 1
- [ 7] .comment PROGBITS 0000000000000000 000000fb
- 000000000000002c 0000000000000001 MS 0 0 1
- [ 8] .note.GNU-stack PROGBITS 0000000000000000 00000127
- 0000000000000000 0000000000000000 0 0 1
- [ 9] .note.gnu.pr[…] NOTE 0000000000000000 00000128
- 0000000000000020 0000000000000000 A 0 0 8
- [10] .eh_frame PROGBITS 0000000000000000 00000148
- 0000000000000040 0000000000000000 A 0 0 8
- [11] .rela.eh_frame RELA 0000000000000000 000003d8
- 0000000000000018 0000000000000018 I 12 10 8
- [12] .symtab SYMTAB 0000000000000000 00000188
- 0000000000000138 0000000000000018 13 5 8
- [13] .strtab STRTAB 0000000000000000 000002c0
- 000000000000003d 0000000000000000 0 0 1
- [14] .shstrtab STRTAB 0000000000000000 000003f0
- 000000000000008a 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)
- 重定位节 .rela.text:
- 包含9个重定位项,每项描述了需要调整的地址、符号类型、符号值等。
- 例如,第一项指向.rodata.str1.8的地址,并在后面加上0。
- R_X86_64_PLT32表示相对地址,需要通过对应的PLT表进行重定位。
- 重定位节 ‘.rela.text’ at offset 0x300 contains 9 entries:
- 偏移量 信息 类型 符号值 符号名称 + 加数
- 00000000001a 00030000000a R_X86_64_32 0000000000000000 .rodata.str1.8 + 0
- 00000000001f 000600000004 R_X86_64_PLT32 0000000000000000 puts - 4
- 000000000029 000700000004 R_X86_64_PLT32 0000000000000000 exit - 4
- 000000000036 00040000000a R_X86_64_32 0000000000000000 .rodata.str1.1 + 0
- 000000000045 000800000004 R_X86_64_PLT32 0000000000000000 __printf_chk - 4
- 000000000058 000900000004 R_X86_64_PLT32 0000000000000000 strtol - 4
- 00000000005f 000a00000004 R_X86_64_PLT32 0000000000000000 sleep - 4
- 00000000006e 000b00000002 R_X86_64_PC32 0000000000000000 stdin - 4
- 000000000073 000c00000004 R_X86_64_PLT32 0000000000000000 getc - 4
- 重定位节 .rela.eh_frame:
- 包含1个重定位项,该项描述了对.text中地址的调整。
- R_X86_64_PC32表示相对地址,需要通过对应的PLT表进行重定位。
- 符号为.text,加数为0,表示对代码段的重定位。
- 重定位节 ‘.rela.eh_frame’ at offset 0x3d8 contains 1 entry:
- 偏移量 信息 类型 符号值 符号名称 + 加数
- 000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
- No processor specific unwind information to decode
- 符号表 .symtab:
- 列出了13个符号的信息,包括值、大小、类型、绑定、可见性、所属的节等。
- 例如,main是一个全局函数,位于.text节中。
- Symbol table ‘.symtab’ contains 13 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 .text
- 3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata.str1.8
- 4: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .rodata.str1.1
- 5: 0000000000000000 131 FUNC GLOBAL DEFAULT 1 main
- 6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
- 7: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
- 8: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __printf_chk
- 9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND strtol
- 10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
- 11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND stdin
- 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getc
- 重定位项分析:
- .rela.text包含了代码段的重定位信息,例如对.rodata.str1.8、puts、exit等的引用。
- 通过.rela.eh_frame对.text中的地址进行重定位。
4.4 Hello.o的结果解析
是用如上指令将hello.o进行反汇编得到如下汇编代码:
hello.o: 文件格式 elf64-x86-64
- Disassembly of section .text:
- 0000000000000000 :
- 0: f3 0f 1e fa endbr64
- 4: 55 push %rbp
- 5: 53 push %rbx
- 6: 48 83 ec 08 sub $0x8,%rsp
- a: 83 ff 04 cmp $0x4,%edi
- d: 75 0a jne 19 <main+0x19>
- f: 48 89 f3 mov %rsi,%rbx
- 12: bd 00 00 00 00 mov $0x0,%ebp
- 17: eb 4d jmp 66 <main+0x66>
- 19: bf 00 00 00 00 mov $0x0,%edi
- 1a: R_X86_64_32 .rodata.str1.8
- 1e: e8 00 00 00 00 call 23 <main+0x23>
- 1f: R_X86_64_PLT32 puts-0x4
- 23: bf 01 00 00 00 mov $0x1,%edi
- 28: e8 00 00 00 00 call 2d <main+0x2d>
- 29: R_X86_64_PLT32 exit-0x4
- 2d: 48 8b 4b 10 mov 0x10(%rbx),%rcx
- 31: 48 8b 53 08 mov 0x8(%rbx),%rdx
- 35: be 00 00 00 00 mov $0x0,%esi
- 36: R_X86_64_32 .rodata.str1.1
- 3a: bf 01 00 00 00 mov $0x1,%edi
- 3f: b8 00 00 00 00 mov $0x0,%eax
- 44: e8 00 00 00 00 call 49 <main+0x49>
- 45: R_X86_64_PLT32 __printf_chk-0x4
- 49: 48 8b 7b 18 mov 0x18(%rbx),%rdi
- 4d: ba 0a 00 00 00 mov $0xa,%edx
- 52: be 00 00 00 00 mov $0x0,%esi
- 57: e8 00 00 00 00 call 5c <main+0x5c>
- 58: R_X86_64_PLT32 strtol-0x4
- 5c: 89 c7 mov %eax,%edi
- 5e: e8 00 00 00 00 call 63 <main+0x63>
- 5f: R_X86_64_PLT32 sleep-0x4
- 63: 83 c5 01 add $0x1,%ebp
- 66: 83 fd 07 cmp $0x7,%ebp
- 69: 7e c2 jle 2d <main+0x2d>
- 6b: 48 8b 3d 00 00 00 00 mov 0x0(%rip),%rdi # 72 <main+0x72>
- 6e: R_X86_64_PC32 stdin-0x4
- 72: e8 00 00 00 00 call 77 <main+0x77>
- 73: R_X86_64_PLT32 getc-0x4
- 77: b8 00 00 00 00 mov $0x0,%eax
- 7c: 48 83 c4 08 add $0x8,%rsp
- 80: 5b pop %rbx
- 81: 5d pop %rbp
- 82: c3 ret
分析并与hello.s进行比较可知:
- 机器语言构成与汇编语言映射关系:
-
机器语言由一系列字节组成,每个字节表示一个特定的机器指令。在反汇编中,每个机器指令都被转换成对应的汇编语言指令。
-
每条指令的机器语言操作码(opcode)对应于汇编语言中的指令助记符。
-
操作数与汇编语言不一致:
-
机器语言中的操作数与汇编语言的寄存器、立即数、内存引用等形式不同,需要通过反汇编来理解。
-
汇编语言的操作数通常以符号或寄存器的形式呈现,而机器语言中使用寄存器和内存地址的编码方式。
-
分支转移(Jump)指令:
-
汇编语言:在汇编语言中,分支转移指令用于改变程序的控制流,例如jmp和jne等。这些指令用于条件或无条件地跳转到目标地址。
-
机器语言:反汇编后,分支转移指令在机器语言中通常以jmp和jne的形式呈现。在示例中,jne指令对应于机器语言的75 0a,表示如果比较结果不相等,则跳转到偏移地址为0a的位置。
-
函数调用指令:
-
汇编语言:函数调用通常使用call指令。在程序中,函数调用会将控制流转移到被调用函数的起始地址,并在调用完成后返回。
-
机器语言:反汇编后,函数调用指令在机器语言中以call的形式呈现。在示例中,call指令对应于机器语言的e8,后面跟着一个相对地址,表示调用的目标函数。
-
重定位项与函数调用关系:
-
在机器语言中,函数调用的地址是相对地址,需要通过重定位项修正。例如,.rela.text中的 R_X86_64_PLT32 项用于修正PLT表中的函数调用地址。
-
例如,.rela.text中的项 R_X86_64_PLT32 puts-0x4 表示对 puts 函数的调用,通过PLT表修正,-0x4 是修正值。
4.5 本章小结
反汇编的结果显示了hello.o的机器语言指令,通过对比第三章中的hello.s汇编源代码,我们可以理解机器语言和汇编语言的映射关系。机器语言由一系列操作码构成,与汇编语言指令助记符对应。操作数在机器语言中的表示形式与汇编语言略有不同。分支转移指令(如jmp和jne)在机器语言中以特定的字节序列表示,而函数调用(如call)通过相对地址和PLT表进行实现,需要通过重定位项修正。通过详细分析重定位项,特别是对函数调用的修正,我们可以深入理解程序控制流程的底层执行机制。这有助于理解程序的二进制表示和机器级指令的运作方式。
(第4章1分)
**
**
第5章 链接
5.1 链接的概念与作用
概念:链接是将多个目标文件(如hello.o)及其依赖库合并为一个可执行文件(如hello)的过程。它包括了地址绑定、符号解析、重定位等步骤,将程序的各个部分整合在一起,以便在运行时正确地执行。
作用:
- 地址绑定: 链接将目标文件中的地址符号(如函数和变量)与实际的内存地址关联起来,解决了各目标文件之间的符号引用问题。
- 符号解析: 解决不同目标文件中的符号冲突,确保每个符号都有唯一的地址。
- 重定位: 链接器修正目标文件中的相对地址,使得程序能够正确地在内存中执行,包括对函数调用和数据引用的修正。
- 库链接: 将程序依赖的库文件链接到可执行文件中,形成最终的独立可执行文件。
- 生成可执行文件: 将链接后的结果输出为一个可执行文件,供操作系统加载和执行。
5.2 在Ubuntu下链接的命令
使用如上命令,将hello.o和其他必要的静态库链接,得到可执行文件hello。
5.3 可执行目标文件hello的格式
- 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: 13616 (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: 28
- Section header string table index: 27
- 节头:
- [号] 名称 类型 地址 偏移量
- 大小 全体大小 旗标 链接 信息 对齐
- [ 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
- 000000000000003c 0000000000000004 A 6 0 8
- [ 5] .gnu.hash GNU_HASH 0000000000400390 00000390
- 0000000000000024 0000000000000000 A 6 0 8
- [ 6] .dynsym DYNSYM 00000000004003b8 000003b8
- 00000000000000f0 0000000000000018 A 7 1 8
- [ 7] .dynstr STRTAB 00000000004004a8 000004a8
- 000000000000007e 0000000000000000 A 0 0 1
- [ 8] .gnu.version VERSYM 0000000000400526 00000526
- 0000000000000014 0000000000000002 A 6 0 2
- [ 9] .gnu.version_r VERNEED 0000000000400540 00000540
- 0000000000000040 0000000000000000 A 7 1 8
- [10] .rela.dyn RELA 0000000000400580 00000580
- 0000000000000048 0000000000000018 A 6 0 8
- [11] .rela.plt RELA 00000000004005c8 000005c8
- 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
- 00000000000000b8 0000000000000000 AX 0 0 16
- [16] .fini PROGBITS 00000000004011a8 000011a8
- 000000000000000d 0000000000000000 AX 0 0 4
- [17] .rodata PROGBITS 0000000000402000 00002000
- 000000000000003b 0000000000000000 A 0 0 8
- [18] .eh_frame PROGBITS 0000000000402040 00002040
- 00000000000000a8 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] .bss NOBITS 0000000000404050 0000304c
- 0000000000000008 0000000000000000 WA 0 0 16
- [24] .comment PROGBITS 0000000000000000 0000304c
- 000000000000002b 0000000000000001 MS 0 0 1
- [25] .symtab SYMTAB 0000000000000000 00003078
- 0000000000000288 0000000000000018 26 7 8
- [26] .strtab STRTAB 0000000000000000 00003300
- 0000000000000145 0000000000000000 0 0 1
- [27] .shstrtab STRTAB 0000000000000000 00003445
- 00000000000000e6 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)
- There are no section groups in this file.
- 程序头:
- 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
- 0x0000000000000658 0x0000000000000658 R 0x1000
- LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
- 0x00000000000001b5 0x00000000000001b5 R E 0x1000
- LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
- 0x00000000000000e8 0x00000000000000e8 R 0x1000
- LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
- 0x00000000000001fc 0x0000000000000208 RW 0x1000
- DYNAMIC 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
- 0x00000000000001a0 0x00000000000001a0 RW 0x8
- NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300
- 0x0000000000000030 0x0000000000000030 R 0x8
- NOTE 0x0000000000000330 0x0000000000400330 0x0000000000400330
- 0x0000000000000020 0x0000000000000020 R 0x4
- GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300
- 0x0000000000000030 0x0000000000000030 R 0x8
- GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
- 0x0000000000000000 0x0000000000000000 RW 0x10
- GNU_RELRO 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
- 0x00000000000001b0 0x00000000000001b0 R 0x1
- Section to Segment mapping:
- 段节…
- 00
- 01 .interp
- 02 .interp .note.gnu.property .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
- 03 .init .plt .plt.sec .text .fini
- 04 .rodata .eh_frame
- 05 .dynamic .got .got.plt .data .bss
- 06 .dynamic
- 07 .note.gnu.property
- 08 .note.ABI-tag
- 09 .note.gnu.property
- 10
- 11 .dynamic .got
- Dynamic section at offset 0x2e50 contains 21 entries:
- 标记 类型 名称/值
- 0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
- 0x000000000000000c (INIT) 0x401000
- 0x000000000000000d (FINI) 0x4011a8
- 0x0000000000000004 (HASH) 0x400350
- 0x000000006ffffef5 (GNU_HASH) 0x400390
- 0x0000000000000005 (STRTAB) 0x4004a8
- 0x0000000000000006 (SYMTAB) 0x4003b8
- 0x000000000000000a (STRSZ) 126 (bytes)
- 0x000000000000000b (SYMENT) 24 (bytes)
- 0x0000000000000015 (DEBUG) 0x0
- 0x0000000000000003 (PLTGOT) 0x404000
- 0x0000000000000002 (PLTRELSZ) 144 (bytes)
- 0x0000000000000014 (PLTREL) RELA
- 0x0000000000000017 (JMPREL) 0x4005c8
- 0x0000000000000007 (RELA) 0x400580
- 0x0000000000000008 (RELASZ) 72 (bytes)
- 0x0000000000000009 (RELAENT) 24 (bytes)
- 0x000000006ffffffe (VERNEED) 0x400540
- 0x000000006fffffff (VERNEEDNUM) 1
- 0x000000006ffffff0 (VERSYM) 0x400526
- 0x0000000000000000 (NULL) 0x0
- 重定位节 ‘.rela.dyn’ at offset 0x580 contains 3 entries:
- 偏移量 信息 类型 符号值 符号名称 + 加数
- 000000403ff0 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0
- 000000403ff8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 gmon_start + 0
- 000000404050 000900000005 R_X86_64_COPY 0000000000404050 stdin@GLIBC_2.2.5 + 0
- 重定位节 ‘.rela.plt’ at offset 0x5c8 contains 6 entries:
- 偏移量 信息 类型 符号值 符号名称 + 加数
- 000000404018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
- 000000404020 000400000007 R_X86_64_JUMP_SLO 0000000000000000 strtol@GLIBC_2.2.5 + 0
- 000000404028 000500000007 R_X86_64_JUMP_SLO 0000000000000000 __printf_chk@GLIBC_2.3.4 + 0
- 000000404030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0
- 000000404038 000700000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
- 000000404040 000800000007 R_X86_64_JUMP_SLO 0000000000000000 getc@GLIBC_2.2.5 + 0
- No processor specific unwind information to decode
- Symbol table ‘.dynsym’ contains 10 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 NOTYPE WEAK DEFAULT UND gmon_start
- 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND […]@GLIBC_2.2.5 (3)
- 5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND […]@GLIBC_2.3.4 (4)
- 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (3)
- 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (3)
- 8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getc@GLIBC_2.2.5 (3)
- 9: 0000000000404050 8 OBJECT GLOBAL DEFAULT 23 stdin@GLIBC_2.2.5 (3)
- Symbol table ‘.symtab’ contains 27 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: 0000000000404050 8 OBJECT GLOBAL DEFAULT 23 stdin@GLIBC_2.2.5
- 11: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 _edata
- 12: 00000000004011a8 0 FUNC GLOBAL HIDDEN 16 _fini
- 13: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 22 __data_start
- 14: 0000000000000000 0 NOTYPE WEAK DEFAULT UND gmon_start
- 15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strtol@GLIBC_2.2.5
- 16: 0000000000402000 4 OBJECT GLOBAL DEFAULT 17 _IO_stdin_used
- 17: 0000000000404058 0 NOTYPE GLOBAL DEFAULT 23 _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 23 __bss_start
- 21: 0000000000401125 131 FUNC GLOBAL DEFAULT 15 main
- 22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __printf_chk@GLI[…]
- 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
- 26: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getc@GLIBC_2.2.5
- Histogram for bucket list length (total of 3 buckets):
- Length Number % of total Coverage
- 0 0 ( 0.0%)
- 1 0 ( 0.0%) 0.0%
- 2 0 ( 0.0%) 0.0%
- 3 3 (100.0%) 100.0%
- Histogram for `.gnu.hash’ bucket list length (total of 2 buckets):
- Length Number % of total Coverage
- 0 1 ( 50.0%)
- 1 1 ( 50.0%) 100.0%
- Version symbols section ‘.gnu.version’ contains 10 entries:
- 地址:0x0000000000400526 Offset: 0x000526 Link: 6 (.dynsym)
- 000: 0 (本地) 2 (GLIBC_2.34) 3 (GLIBC_2.2.5) 1 (全局)
- 004: 3 (GLIBC_2.2.5) 4 (GLIBC_2.3.4) 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
- 008: 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
- Version needs section ‘.gnu.version_r’ contains 1 entry:
- 地址:0x0000000000400540 Offset: 0x000540 Link: 7 (.dynstr)
- 000000: Version: 1 文件:libc.so.6 计数:3
- 0x0010: Name: GLIBC_2.3.4 标志:无 版本:4
- 0x0020: Name: GLIBC_2.2.5 标志:无 版本:3
- 0x0030: Name: GLIBC_2.34 标志:无 版本:2
- Displaying notes found in: .note.gnu.property
- 所有者 Data size Description
- GNU 0x00000020 NT_GNU_PROPERTY_TYPE_0
- Properties: x86 feature: IBT, SHSTK
- x86 ISA needed: x86-64-baseline
- Displaying notes found in: .note.ABI-tag
- 所有者 Data size Description
- GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
- OS: Linux, ABI: 3.2.0
ELF 头:
- 类型: ELF64
- 数据: 补码,小端序 (little endian)
- 版本: 1 (current)
- OS/ABI: UNIX - System V
- ABI 版本: 0
- 入口点地址: 0x4010f0
- 程序头起点: 64 (bytes into file)
- Start of section headers: 13616 (bytes into file)
- 头的大小: 64 (bytes)
- 程序头的大小: 56 (bytes)
- 程序头的数量: 12
- 节头的大小: 64 (bytes)
- 节头的数量: 28
- 节头字符串表索引: 27
节头:
- .interp:地址 0x00000000004002e0,大小 0x1c,类型 PROGBITS,标志 A
- .note.gnu.pr[…]:地址 0x0000000000400300,大小 0x30,类型 NOTE,标志 A
- .note.ABI-tag:地址 0x0000000000400330,大小 0x20,类型 NOTE,标志 A
- .hash:地址 0x0000000000400350,大小 0x3c,类型 HASH,标志 A
- .gnu.hash:地址 0x0000000000400390,大小 0x24,类型 GNU_HASH,标志 A
- .dynsym:地址 0x00000000004003b8,大小 0xf0,类型 DYNSYM,标志 A
- .dynstr:地址 0x00000000004004a8,大小 0x7e,类型 STRTAB,标志 A
- .gnu.version:地址 0x0000000000400526,大小 0x14,类型 VERSYM,标志 A
- .gnu.version_r:地址 0x0000000000400540,大小 0x40,类型 VERNEED,标志 A
- .rela.dyn:地址 0x0000000000400580,大小 0x48,类型 RELA,标志 A
- .rela.plt:地址 0x00000000004005c8,大小 0x90,类型 RELA,标志 AI
- .init:地址 0x0000000000401000,大小 0x1b,类型 PROGBITS,标志 AX
- .plt:地址 0x0000000000401020,大小 0x70,类型 PROGBITS,标志 AX
- .plt.sec:地址 0x0000000000401090,大小 0x60,类型 PROGBITS,标志 AX
- .text:地址 0x00000000004010f0,大小 0xb8,类型 PROGBITS,标志 AX
- .fini:地址 0x00000000004011a8,大小 0xd,类型 PROGBITS,标志 AX
- .rodata:地址 0x0000000000402000,大小 0x3b,类型 PROGBITS,标志 A
- .eh_frame:地址 0x0000000000402040,大小 0xa8,类型 PROGBITS,标志 A
- .dynamic:地址 0x0000000000403e50,大小 0x1a0,类型 DYNAMIC,标志 WA
- .got:地址 0x0000000000403ff0,大小 0x10,类型 PROGBITS,标志 WA
- .got.plt:地址 0x0000000000404000,大小 0x48,类型 PROGBITS,标志 WA
- .data:地址 0x0000000000404048,大小 0x4,类型 PROGBITS,标志 WA
- .bss:地址 0x0000000000404050,大小 0x8,类型 NOBITS,标志 WA
程序头:
- PHDR:偏移 0x40,虚拟地址 0x400040,物理地址 0x400040,文件大小 0x2a0,内存大小 0x2a0,标志 R,对齐 0x8
- INTERP:偏移 0x2e0,虚拟地址 0x4002e0,物理地址 0x4002e0,文件大小 0x1c,内存大小 0x1c,标志 R,对齐 0x1
- LOAD:偏移 0,虚拟地址 0x400000,物理地址 0x400000,文件大小 0x658,内存大小 0x658,标志 R,对齐 0x1000
- LOAD:偏移 0x1000,虚拟地址 0x401000,物理地址 0x401000,文件大小 0x1b5,内存大小 0x1b5,标志 R E,对齐 0x1000
- LOAD:偏移 0x2000,虚拟地址 0x402000,物理地址 0x402000,文件大小 0xe8,内存大小 0xe8,标志 R,对齐 0x1000
- LOAD:偏移 0x2e50,虚拟地址 0x403e50,物理地址 0x403e50,文件大小 0x1fc,内存大小 0x208,标志 RW,对齐 0x1000
- DYNAMIC:偏移 0x2e50,虚拟地址 0x403e50,物理地址 0x403e50,文件大小 0x1a0,内存大小 0x1a0,标志 RW,对齐 0x8
- NOTE:偏移 0x300,虚拟地址 0x400300,物理地址 0x400300,文件大小 0x30,内存大小 0x30,标志 R,对齐 0x8
- NOTE:偏移 0x330,虚拟地址 0x400330,物理地址 0x400330,文件大小 0x20,内存大小 0x20,标志 R,对齐 0x4
- GNU_PROPERTY:偏移 0x300,虚拟地址 0x400300,物理地址 0x400300,文件大小 0x30,内存大小 0x30,标志 R,对齐 0x8
- GNU_STACK:偏移 0,虚拟地址 0,物理地址 0,文件大小 0,内存大小 0,标志 RW,对齐 0x10
- GNU_RELRO:偏移 0x2e50,虚拟地址 0x403e50,物理地址 0x403e50,文件大小 0x1b0,内存大小 0x1b0,标志 R,对齐 0x1
5.4 hello的虚拟地址空间
虚拟地址空间各段信息可以看出,前4个段时hello本身的内容,对应5.3中readelf查看的hello的程序头段的4个LOAD:
- LOAD:偏移 0,虚拟地址 0x400000,物理地址 0x400000,文件大小 0x658,内存大小 0x658,标志 R,对齐 0x1000
- LOAD:偏移 0x1000,虚拟地址 0x401000,物理地址 0x401000,文件大小 0x1b5,内存大小 0x1b5,标志 R E,对齐 0x1000
- LOAD:偏移 0x2000,虚拟地址 0x402000,物理地址 0x402000,文件大小 0xe8,内存大小 0xe8,标志 R,对齐 0x1000
- LOAD:偏移 0x2e50,虚拟地址 0x403e50,物理地址 0x403e50,文件大小 0x1fc,内存大小 0x208,标志 RW,对齐 0x1000
接下来4个段来自hello加载的动态链接库ld-linux-x86-64.so.2,剩下的则是栈段和内核相关段。
5.5 链接的重定位过程分析
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 puts@plt:
- 401090: f3 0f 1e fa endbr64
- 401094: f2 ff 25 7d 2f 00 00 bnd jmp *0x2f7d(%rip) # 404018 <puts@GLIBC_2.2.5>
- 40109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010a0 strtol@plt:
- 4010a0: f3 0f 1e fa endbr64
- 4010a4: f2 ff 25 75 2f 00 00 bnd jmp *0x2f75(%rip) # 404020 <strtol@GLIBC_2.2.5>
- 4010ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010b0 __printf_chk@plt:
- 4010b0: f3 0f 1e fa endbr64
- 4010b4: f2 ff 25 6d 2f 00 00 bnd jmp *0x2f6d(%rip) # 404028 <__printf_chk@GLIBC_2.3.4>
- 4010bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010c0 exit@plt:
- 4010c0: f3 0f 1e fa endbr64
- 4010c4: f2 ff 25 65 2f 00 00 bnd jmp *0x2f65(%rip) # 404030 <exit@GLIBC_2.2.5>
- 4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010d0 sleep@plt:
- 4010d0: f3 0f 1e fa endbr64
- 4010d4: f2 ff 25 5d 2f 00 00 bnd jmp *0x2f5d(%rip) # 404038 <sleep@GLIBC_2.2.5>
- 4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 00000000004010e0 getc@plt:
- 4010e0: f3 0f 1e fa endbr64
- 4010e4: f2 ff 25 55 2f 00 00 bnd jmp *0x2f55(%rip) # 404040 <getc@GLIBC_2.2.5>
- 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: 53 push %rbx
- 40112b: 48 83 ec 08 sub $0x8,%rsp
- 40112f: 83 ff 04 cmp $0x4,%edi
- 401132: 75 0a jne 40113e <main+0x19>
- 401134: 48 89 f3 mov %rsi,%rbx
- 401137: bd 00 00 00 00 mov $0x0,%ebp
- 40113c: eb 4d jmp 40118b <main+0x66>
- 40113e: bf 08 20 40 00 mov $0x402008,%edi
- 401143: e8 48 ff ff ff call 401090 puts@plt
- 401148: bf 01 00 00 00 mov $0x1,%edi
- 40114d: e8 6e ff ff ff call 4010c0 exit@plt
- 401152: 48 8b 4b 10 mov 0x10(%rbx),%rcx
- 401156: 48 8b 53 08 mov 0x8(%rbx),%rdx
- 40115a: be 2e 20 40 00 mov $0x40202e,%esi
- 40115f: bf 01 00 00 00 mov $0x1,%edi
- 401164: b8 00 00 00 00 mov $0x0,%eax
- 401169: e8 42 ff ff ff call 4010b0 __printf_chk@plt
- 40116e: 48 8b 7b 18 mov 0x18(%rbx),%rdi
- 401172: ba 0a 00 00 00 mov $0xa,%edx
- 401177: be 00 00 00 00 mov $0x0,%esi
- 40117c: e8 1f ff ff ff call 4010a0 strtol@plt
- 401181: 89 c7 mov %eax,%edi
- 401183: e8 48 ff ff ff call 4010d0 sleep@plt
- 401188: 83 c5 01 add $0x1,%ebp
- 40118b: 83 fd 07 cmp $0x7,%ebp
- 40118e: 7e c2 jle 401152 <main+0x2d>
- 401190: 48 8b 3d b9 2e 00 00 mov 0x2eb9(%rip),%rdi # 404050 <stdin@GLIBC_2.2.5>
- 401197: e8 44 ff ff ff call 4010e0 getc@plt
- 40119c: b8 00 00 00 00 mov $0x0,%eax
- 4011a1: 48 83 c4 08 add $0x8,%rsp
- 4011a5: 5b pop %rbx
- 4011a6: 5d pop %rbp
- 4011a7: c3 ret
- Disassembly of section .fini:
- 00000000004011a8 <_fini>:
- 4011a8: f3 0f 1e fa endbr64
- 4011ac: 48 83 ec 08 sub $0x8,%rsp
- 4011b0: 48 83 c4 08 add $0x8,%rsp
- 4011b4: c3 ret
差异分析:
- 存在的段:
- 在 hello 可执行文件中,新增了 .plt.sec 段,而在 hello.o 目标文件中不存在。这表明在链接过程中可能引入了一些新的内容,这可能与库的动态链接有关。
- .init 和 .fini 段在 hello 可执行文件中存在,但在 hello.o 目标文件中不存在。这两个段包含了在程序启动和结束时执行的代码,它们是链接器添加的附加部分。
2. 符号和重定位项:
- hello.o 文件包含了一些重定位项,如 R_X86_64_32 和 R_X86_64_PLT32,它们表明了对动态链接库的引用和重定位。例如,R_X86_64_32 .rodata.str1.8 表示一个对 .rodata.str1.8 地址的引用,而 R_X86_64_PLT32 puts-0x4 表示对 puts 函数的引用。
- 在 hello 可执行文件中,这些符号在 .plt 和 .plt.sec 段中得到了解析和填充。例如,.plt 段包含了对动态函数 puts、strtol、__printf_chk 等的 PLT 条目,这些 PLT 条目中的地址在运行时由动态链接器解析和填充。
3. 过程链接表(PLT**):**
- 在 hello 可执行文件中,.plt 段包含了对动态函数的条目,这些条目用于延迟绑定。例如,.plt 中的条目调用 puts 函数的地址。这种设计允许在程序运行时动态解析和链接函数。
- .plt.sec 段也包含了对其他函数的类似条目。这些都是为了实现动态链接和延迟绑定。
4. 地址解析和动态链接:
- 在 hello 可执行文件中,动态链接器在程序启动时解析和填充了动态函数的地址。这样,在程序执行过程中,对这些函数的调用将会使用正确的地址。
- 例如,在 hello 的 .plt 段中,puts@plt 条目通过 bnd jmp 指令调用了真正的 puts 函数地址。这个过程是在程序运行时由动态链接器完成的。
5. 其他细节:
- .init 段包含了程序初始化时执行的代码,而 .fini 段包含了程序终止时执行的代码。这些段的存在表明在链接过程中加入了一些用于程序初始化和终止的逻辑。
链接过程:
- 编译:
- 源代码编译为目标文件,即生成了hello.o。
- 链接:
- 链接过程将目标文件组合起来,并解析符号以创建最终的可执行文件(hello)。
- 在链接期间,动态符号(如 puts、strtol 等)是未解析的,会在.plt段中用占位符表示。
- 地址解析:
- 动态链接器在运行时解析这些符号的地址,并更新.plt段中的实际地址。
- 添加到可执行文件的段:
- .init 和 .fini 段是可执行文件的一部分,包含在初始化和终止程序时执行的代码。
- 过程链接表(PLT**):**
- 可执行文件中的.plt段包含了对动态函数的条目。当调用函数时,对应的PLT条目被执行,动态链接器会解析并更新实际地址。
5.6 hello的执行流程
首先程序从地址0x7fa17c938290处开始运行。这一部分的代码属于动态链接库ld-linux-x86-64.so.2。这一过程调用了两个函数,其地址分别为0x7f5c2d8d6030和0x7f5c2d8bb4d0。经过一系列初始化后。程序跳转到_start。
_start的地址存在寄存器R12中,其地址是0x4010f0。然后程序通过一个间接call指令跳到动态链接库libc.so.6的__libc_start_main处,这个函数会进行一些初始化,并负责调用main函数。其地址为0x7f7cec829dc0。
然后程序调用动态库libc.so.6中的__cxa_atexit函数,它会设置程序结束时需要调用的函数表。其地址为0x7f7cec8458c0。该过程中也会调用动态库中的其他代码。
然后程序返回到__libc_start_main继续运行。
然后程序跳转到_init函数进行一系列初始化操作。其地址为0x401000。
接着程序返回到__libc_start_main继续运行。
然后调用动态链接库里的_setjmp函数,应该是设置一些非本地跳转。其地址为0x7f18ed8421e0。
然后返回到__libc_start_main继续,然后就开始调用main函数。
由于我们没有给出命令行参数,程序运行到第一个判断之后直接调用exit退出程序。其地址为0x404030。
然后程序会调用动态库中的一些代码。最后程序退出。
5.7 Hello的动态链接分析
在Hello程序中,动态链接分析是通过延迟绑定的方法实现的。当程序调用由共享库定义的函数时,编译器无法预测函数的地址,因此编译系统采用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。这一过程通过全局偏移表(GOT)和过程链接表(PLT)的协作来解析函数的地址。
在加载时,动态链接器会重定位GOT中的每个条目,确保它包含正确的绝对地址。同时,PLT中的每个函数负责调用不同的函数。因此,通过观察调试器(如edb),可以发现在dl_init
之后,.got.plt
节会发生变化,其中包含了正确绑定的地址信息。
对于Hello程序,当加载可执行文件时,自动加载了动态链接库ld-2.31.so
。在程序的某些地方可能会引用该动态链接库中的符号,比如函数调用。这一引用机制通过PLT表和GOT表实现,其中每个表的条目对应动态链接库中的符号引用。在readelf
中,可以通过查看节表找到关于PLT和GOT的信息,这两者分别对应于不同的节。
程序一开始执行_dl_start
和_dl_init
,其中_dl_init
可修改PLT和GOT,相当于“注册”动态链接库的符号。这确保在程序正常运行中,hello可以引用这些符号,实现间接跳转等行为。通过使用edb监测0x404000地址处PLT的数据变化,可以验证这一过程,而这是在调用_dl_init
之前的状态。
这是在调用_dl_init
之后的状态。
通过对比可以发现两者的不同之处。
5.8 本章小结
在链接过程中,代码和数据片段通过链接器组合成一个可执行文件,使得分离编译成为可能。这允许将应用程序组织为小的管理模块,在应用时通过链接完成整个任务。经过链接后,得到可执行文件,通过在shell中调用命令即可创建进程执行该文件。
通过分析hello.o、静态库和动态链接库的链接机制,我们深入了解了C语言程序从加载到退出的整个过程。静态库和动态链接库在程序中的作用是隐藏在我们视线之外,使得hello程序的复杂性超出表面看似。这种复杂性得益于链接机制,使我们能够方便地利用库来编写程序,使其在操作系统提供的平台上正常运行。链接机制为程序提供了灵活性和便利性,使得代码的组织和执行更加高效。
(第5章1分)
**
**
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机系统中的基本概念,代表正在执行的程序实例。每个进程具有独立的地址空间、资源、代码和数据,确保它们互相隔离,不会相互干扰。进程的存在使操作系统能够实现多任务并发执行,提高系统效率和吞吐量。通过任务调度算法,操作系统决定何时执行哪个进程,实现资源的公平分配。进程也促使了通信与同步,进程间可以通过进程间通信(IPC)进行数据传递和协作,同时避免冲突。独立的地址空间和资源拥有者特性提高了系统的稳定性和安全性,防止一个进程的错误影响其他进程。此外,进程支持多用户环境,不同用户的任务通过独立的进程进行执行,实现了用户间的隔离。总体而言,进程在计算机系统中发挥着关键作用,通过并发执行、资源管理、通信和隔离等功能,为系统提供了灵活性、效率和稳定性。
6.2 简述壳Shell-bash的作用与处理流程
Shell(如bash)是计算机操作系统中的命令解释器,主要作用是接收用户输入的命令并将其翻译为可执行的操作系统指令。它支持命令解释与执行、脚本编程、环境变量管理、I/O重定向和管道等功能。Shell通过解析用户输入,查找可执行程序,创建进程执行命令,并等待命令完成的过程,实现用户与操作系统的交互。通过Shell,用户可以方便地执行系统命令、编写自动化脚本、管理环境变量,实现更灵活和高效的计算机操作。以下是它的处理流程[1]:
- 接收用户输入: Shell等待用户输入,用户键入命令并按下回车键。
- 解析命令: Shell对用户输入的命令进行解析,将其分解为可执行程序、参数以及其他选项。
- 查找可执行程序: Shell搜索系统的PATH路径,以确定要执行的可执行程序的路径。
- 创建进程: Shell创建一个新的进程,用于执行用户指定的命令。这通常是通过fork系统调用实现的。
- 执行命令: 新进程执行用户指定的命令,调用相应的系统调用与操作系统进行交互,执行具体的操作。
- 等待命令完成: Shell等待子进程执行完毕,这可能包括等待子进程返回状态,以确定命令的执行成功与否。
- 显示结果: 根据命令的执行结果,Shell可能输出命令的标准输出或标准错误,或者显示其他与命令执行相关的信息。
- 返回等待用户输入: 处理完一条命令后,Shell再次等待用户输入,形成一个循环,等待用户下一次命令。
窗体顶端
6.3 Hello的fork进程创建过程
fork 是一个系统调用,用于在Unix和类Unix操作系统中创建一个新的进程。fork 调用的过程如下:
- 调用 fork: 进程通过调用系统调用 fork 来创建一个新的进程。在调用 fork 时,操作系统会复制当前进程的内存空间,创建一个几乎完全相同的子进程。
- pid_t child_pid = fork();
这里 child_pid 会存储 fork 的返回值,如果是子进程,返回值是 0,如果是父进程,返回值是新创建的子进程的进程ID。
- 复制地址空间: 操作系统会复制父进程的地址空间,包括代码段、数据段、堆和栈。子进程与父进程共享相同的程序代码,但有各自的数据副本。
- 设置返回值: 子进程和父进程都从 fork 调用中得到一个返回值。在子进程中,返回值是 0,而在父进程中,返回值是新创建子进程的进程ID。
- 执行代码: 接下来,子进程和父进程都开始执行代码。由于它们有相同的内存内容,但是有不同的进程ID,因此可以通过返回值来区分它们。
在这个过程中,子进程继承了父进程的文件描述符、信号处理器等属性。需要注意的是,fork 的复制是对父进程的复制,而不是整个系统的复制,因此新进程的创建是相对轻量级的。
通常,父进程和子进程之间的执行顺序是不确定的,取决于操作系统的调度策略。在实际编程中,为了更好地控制进程的执行,可能需要使用其他机制,如 wait 函数等。窗体顶端
6.4 Hello的execve过程
execve 是一个系统调用,用于在Unix和类Unix操作系统中执行一个新的程序。该调用会用一个新的程序替代当前进程的内容。以下是 execve 的主要过程:
- 包含头文件: 在程序中包含头文件 unistd.h,该头文件包含 execve 的声明。
- 准备参数: 准备一个包含新程序的路径、命令行参数以及环境变量的数组。这些参数被传递给 execve 函数。
- 调用 execve: 调用 execve 函数,传递程序路径、命令行参数和环境变量。如果 execve 成功执行,它将不会返回,而是直接将当前进程替换为新程序,否则,它将返回 -1,表示出现了错误。
- 错误处理: 如果 execve 返回 -1,则表示执行出错。可以使用 perror 或其他错误处理机制输出错误信息。
总体而言,execve 的过程涉及准备参数并调用系统调用,将当前进程替换为新的程序。这是一种常见的用于创建新进程并执行其他程序的方式,通常在调用 fork 之后,子进程使用 execve 来加载新的程序。
6.5 Hello的进程执行
进程执行的过程涉及到进程上下文信息、进程时间片、用户态与核心态的转换等方面。以下是进程执行和调度的主要过程:
- 进程上下文信息: 操作系统为每个进程维护了进程上下文信息,包括程序计数器(PC)、寄存器状态、内存映像、打开的文件等。当一个进程被调度执行时,它的上下文信息被加载到CPU寄存器和内存中。
- 进程时间片: 操作系统通常使用时间片轮转调度算法来分配CPU时间给各个进程。每个进程被分配一个时间片,当时间片用完时,操作系统将当前进程放入就绪队列,切换到下一个就绪进程执行。
- 进程调度过程:
-
就绪队列: 进程在等待CPU执行的过程中处于就绪状态,排队在就绪队列中。
-
选择进程: 调度器选择一个就绪进程来执行,通常采用各种调度算法,如先来先服务(FCFS)、最短作业优先(SJF)或轮转调度等。
-
加载上下文: 选择的进程的上下文信息被加载到CPU,包括程序计数器、寄存器等。
-
执行进程: CPU执行被选中的进程,执行对应的指令集。
-
用户态与核心态转换: CPU在执行进程时可能处于用户态或核心态。
-
用户态: 进程执行普通的用户程序,只能访问受限的资源。
-
核心态: 进程执行特权指令,可以访问系统资源、执行I/O操作等。转换通常由系统调用或异常引起。
-
系统调用: 当用户程序需要执行特权指令时,如进行I/O操作或申请系统资源,它会发起系统调用请求。这将导致从用户态切换到核心态,操作系统响应系统调用,并执行相应的内核代码。
-
中断和异常: 进程执行过程中,可能发生中断或异常,例如时钟中断、硬件中断或执行非法指令。这将导致从用户态切换到核心态,操作系统处理中断或异常,然后恢复进程执行。
综合而言,进程执行涉及进程上下文的加载、时间片调度、用户态与核心态的转换等过程,操作系统通过这些机制来实现多任务并发执行,有效地利用计算机资源。
6.6 hello的异常与信号处理
正常执行状态:
异常类型:
类别 | 原因 | 异步/同步 | 返回行为 |
---|---|---|---|
中断 | 来自 I/O 设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
处理方式:
- 中断
中断是异步发生的,是来自处理器外部I/O设备的信号的结果。硬件中断的异常处理程序常常成为中断处理程序。
- 陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。
陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。执行syscall指令会导致一个到异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核程序(不是直接跳到内核程序)。
从程序员角度来看系统调用和普通的函数调用是一样的。实际上,系统调用运行在内核模式中,内核模式允许系统调用执行特权指令,并访问定义在内核中的栈。
- 故障
故障由错误情况引起,它可能能够被故障处理程序修正。一个经典的故障是缺页异常。
故障发生时,处理器将控制转移给故障处理程序。若能修正,则重新执行引起故障的指令;否则返回到内核中的abort例程,终止程序。
- 终止
终止是不可恢复的致命错误造成的结果。通常是一些硬件错误。
终止处理程序从不将控制返回给应用程序。
不停乱按:将屏幕的输入缓存到缓冲区。乱码被认为是命令,不影响当前进程的执行
按下 Ctrl-Z:程序运行时按 Ctrl-Z,这时,产生中断异常,它的父进程会接收到信号 SIGSTP 并运行信号处理程序,然后便发现程序在这时被挂起了,并打印了相关挂起信息。
Ctrl-Z 后运行 ps,打印出了各进程的 pid,可以看到之前挂起的进程 hello。
Ctrl-Z 后运行 jobs,打印出了被挂起进程组的 jid,可以看到之前被挂起的 hello,以被挂起的标识 Stopped。
Ctrl-Z 后运行 pstree,可看到它打印出的信息
Ctrl-Z 后运行 fg:因为之前运行 jobs 是得知 hello 的 jid 为 1,那么运行 fg 1 可以把之前挂起在后台的hello重新调到前台来执行,打印出剩余部分,然后输入hello回车,程序运行结束,进程被回收。
Ctrl-Z 后运行 Kill:重新执行进程,可以发现 hello 的进程号为 8514通过 kill -9 8514号 SIGKILL 给进程 8514致该进程被杀死。然后再运行 ps,可发现已被杀死的进程 hello。
按下 Ctrl-C:进程收到 SIGINT 信号,结束 hello。在 ps 中查询不到其 PID,在 job 中也没有显示,可以看出 hello 已经被彻底结束。
6.7本章小结
本章详细阐述了可执行文件"hello"的执行过程,包括进程创建、加载和终止,以及通过键盘输入等互动。在整个执行过程中,各种异常和中断等信息扮演重要角色。程序的高效运行关键在于对异常、信号和进程等概念的深刻理解与应用。这些机制的支持使得"hello"能够顺利在计算机上执行。进程创建通过系统调用实现,加载则包括从可执行文件读取指令并载入内存。终止可能因程序执行完毕或接收到信号而触发。键盘输入引入了用户与程序的交互,例如Ctrl-C和Ctrl-Z触发的中断和暂停信号。总体而言,本章突显了程序执行的复杂性,以及通过异常处理和信号传递等机制的设计,为程序在计算机上的顺畅运行提供了关键支持。
(第6章1分)
**
**
第7章 hello的存储管理
7.1 hello的存储器地址空间
在计算机中,"hello"可执行文件在存储器中有其自己的地址空间,包括逻辑地址、线性地址、虚拟地址和物理地址等不同概念。
- 逻辑地址: 这是程序员在编写程序时使用的地址。在"hello"的源代码中,变量和指令使用的都是逻辑地址。这些地址是相对于程序的起始位置而言的。
- 线性地址: 也称为虚拟地址,是程序在运行时使用的地址。通过地址映射,操作系统将线性地址转换为物理地址。在程序执行过程中,线性地址空间允许程序访问所有分配给它的虚拟地址。
- 虚拟地址: 这是用户程序中使用的地址。操作系统通过地址映射将虚拟地址转换为物理地址。虚拟地址通常是相对于程序的基地址,不直接映射到物理内存。
- 物理地址: 最终数据在计算机内存中的实际地址。通过内存管理单元(MMU)和操作系统的地址映射机制,虚拟地址转换为物理地址,这样程序可以在实际的硬件内存中找到相应的数据和指令。
对于"hello"程序,编译器和链接器将其源代码转换为可执行文件时,分配了逻辑地址。当程序执行时,操作系统通过地址映射将逻辑地址转换为线性地址,再将线性地址转换为虚拟地址,最终通过MMU将虚拟地址映射到物理地址。这个过程确保了程序能够正确且安全地访问系统内存,实现了逻辑地址到物理地址的有效映射。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel x86 架构使用的是一种称为段式管理(Segmentation)的内存管理方式。在这个模式下,逻辑地址(Logical Address)到线性地址(Linear Address)的变换主要涉及两个步骤:分段和分页。
- 分段(Segmentation):
-
逻辑地址: 在段式管理中,逻辑地址由段选择器(Segment Selector)和偏移量(Offset)组成。段选择器指明了段的起始地址,而偏移量表示从起始地址处的偏移量。
-
段描述符表(Global Descriptor Table - GDT): GDT 存储了各个段的描述符,其中包括段的起始地址、大小、权限等信息。通过段选择器在 GDT 中找到对应的段描述符。
-
线性地址: 段描述符中的基地址与偏移量相加,得到线性地址。
-
分页(Paging):
-
线性地址: 线性地址由段式管理中的结果进入分页单元。
-
页表(Page Table): 页表存储了线性地址到物理地址的映射关系。操作系统通过页表将线性地址转换为物理地址。
-
物理地址: 通过线性地址在页表中查找,得到物理地址。
这两个步骤结合起来,完成了从逻辑地址到线性地址的变换。Intel x86 架构采用这种段式管理和页式管理的结合方式,这种方式兼顾了灵活性和效率。然而,这个模式在现代操作系统中逐渐被更先进的分页机制(如IA-32e模式下的长模式)所取代,以提高内存管理的灵活性和效率。
7.3 Hello的线性地址到物理地址的变换-页式管理
在 Intel x86 架构中,线性地址到物理地址的变换主要通过页式管理(Paging)来实现。这个过程涉及以下几个关键步骤:
- 线性地址(Linear Address): 在程序执行时,CPU生成的地址被称为线性地址。这是一个32位的地址,它被分割成目录部分、表部分和偏移量。
- 页目录表(Page Directory Table): 线性地址的高10位作为索引用于查找页目录表。页目录表中的每一项指向一个页表。
- 页表(Page Table): 通过线性地址的接下来的10位作为索引,访问相应的页表。页表的每一项存储了物理页框的基地址。
- 物理页框(Physical Page Frame): 线性地址的最后12位用作偏移量,加上页表中的基地址,得到最终的物理地址。这表示实际存储数据的物理内存位置。
这种页式管理的机制允许操作系统将线性地址空间分割成大小固定的页面,并将这些页面映射到物理内存的不同区域。这种分页机制带来了以下优势:
- 虚拟内存: 允许每个进程有自己的独立地址空间,而不受物理内存的限制。
- 内存保护: 可以通过对页表的权限设置来保护不同的内存区域,防止非法访问。
- 内存共享: 多个进程可以共享相同的物理内存页,提高系统的内存利用率。
这样的页式管理机制是现代操作系统中常见的内存管理方式,提高了系统的稳定性、安全性和效率。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(Translation Lookaside Buffer)和四级页表结合起来支持虚拟地址(VA)到物理地址(PA)的变换。以下是这个过程的简要描述:
- 虚拟地址(VA): 程序在运行时生成的地址,通常是32或64位的。
- 四级页表:
-
第一级页表: 高位的一部分用于索引第一级页表,它的目的是为了找到第二级页表的起始地址。
-
第二级页表: 继续使用一部分地址索引第二级页表,以找到第三级页表的起始地址。
-
第三级页表: 同样的道理,用于找到第四级页表的起始地址。
-
第四级页表: 包含物理页框的基地址,通过它来定位物理内存中的具体页面。
-
TLB缓存:
-
TLB是一个硬件缓存,用于存储最近访问的虚拟地址到物理地址的映射。
-
当CPU要访问某个虚拟地址时,首先查看TLB,如果在TLB中找到了对应的映射,就可以直接得到物理地址,从而减少了访问页表的开销。
-
如果在TLB中没有找到对应的映射,就需要通过访问页表层层索引,直至找到物理地址。
-
TLB缺失(TLB Miss):
-
当TLB中没有找到所需的映射时,触发TLB缺失。
-
操作系统会介入,将正确的映射加载到TLB中。
这个整个过程实现了虚拟地址到物理地址的转换。TLB的存在加速了这个过程,因为它提供了一个快速查找最近映射的机制,减少了频繁访问页表的开销。四级页表提供了更灵活的地址映射,同时允许操作系统对内存进行更精确的管理。这种组合使得现代操作系统能够在大型内存环境中高效地管理虚拟内存。
7.5 三级Cache支持下的物理内存访问
在一个计算机系统中,通常有多级缓存(例如 L1、L2 和 L3 Cache)以提供不同层次的缓存支持。下面是基于三级 Cache 支持下的物理内存访问的简要描述:
- CPU核心访问:
-
当CPU核心需要访问某个特定的物理内存位置时,首先检查 L1 Cache。
-
如果数据在 L1 Cache 中命中(Hit),则CPU直接从这个最快速的缓存中获取所需数据,无需访问其他缓存或内存。
-
L1 Cache未命中:
-
如果在 L1 Cache 中未找到所需数据(缺失,Miss),则CPU会继续检查 L2 Cache。
-
L2 Cache 通常较大,但相对较慢,它可能包含 L1 Cache 中未命中的数据。
-
L2 Cache未命中:
-
如果在 L2 Cache 中也未找到所需数据,则继续检查 L3 Cache。
-
L3 Cache 是一个更大但相对较慢的缓存,它可能包含 L2 Cache 中未命中的数据。
-
L3 Cache未命中:
-
如果在 L3 Cache 中未找到所需数据,则发生了 L3 Cache Miss。
-
在这种情况下,CPU将访问主内存(RAM)来获取所需数据。
-
内存访问:
-
内存访问是最慢的,因为主内存的访问速度相对较低。
-
一旦数据被加载到 L3 Cache,它可以在后续的访问中快速被 CPU 访问。
整个过程中,缓存的层次结构提供了多级别的快速存储,以降低访问主内存的频率。每一级缓存的大小、速度和连接方式都有所不同,以在不同的工作负载和访问模式下提供最佳性能。通过这种层次化的缓存设计,系统能够更有效地利用局部性原理,提高内存访问的速度和效率。
7.6 hello进程fork时的内存映射
在调用fork函数时,操作系统为新进程创建独立的执行环境。这过程中,内核生成新的进程控制块,分配唯一的PID,并复制当前进程的mm_struct、区域结构和页表,确保新进程有自己的虚拟内存。关键在于通过只读标记和写时复制机制,在初始状态下,新进程与父进程共享相同的物理页面,实现了内存的节省和效率。
标记为只读的页面保证了在初始阶段两个进程共享相同的内存内容。写时复制机制使得当任一进程试图进行写操作时,系统会为其创建一个新的物理页面,确保修改的是私有的副本而不影响其他进程。这样,两个进程在逻辑上保持了私有地址空间的抽象概念。
这种设计在节省内存的同时提高了效率,因为只有在必要时才进行实际的内存复制。这种写时复制的机制是现代操作系统中实现fork的常见方式,充分利用了操作系统的虚拟内存管理机制,使得进程的创建更为高效和灵活。
7.7 hello进程execve时的内存映射
execve函数在加载并运行可执行目标文件"hello"时,替代了当前程序。这一过程包括删除已存在的用户区域、映射私有区域和映射共享区域。首先,已存在的用户区域结构被清除,然后为新程序"hello"的代码、数据、bss和栈区域创建私有、写时复制的新区域结构。这些区域映射到"hello"的.text和.data节,bss区域映射到匿名文件,并初始化栈和堆为空。如果"hello"链接了动态链接库,它们会被映射到共享区域。最终,程序计数器(PC)被设置到"hello"的代码入口点,完成了对新程序的加载和运行。
7.8 缺页故障与缺页中断处理
缺页故障(Page Fault)是指当程序试图访问虚拟内存中的某个页面,而该页面当前未加载到物理内存中时发生的异常。缺页故障的处理通常包括以下步骤:
- 缺页中断触发: 当程序访问虚拟内存中的某个页面,但该页面未在物理内存中时,CPU会触发缺页中断,将控制权交给操作系统内核。
- 中断处理程序执行: 操作系统内核中有一个专门处理缺页中断的中断服务例程。这个例程负责确定缺失页面的原因,并采取相应的措施。
- 页面加载: 如果缺页是由于页面不在物理内存中引起的,操作系统需要将页面从磁盘或其他次级存储装载到物理内存中。这个过程称为页面调度(Page Replacement)。
- 更新页表: 一旦页面加载到物理内存,操作系统更新页表,建立虚拟地址到物理地址的映射关系,以便程序能够正确访问该页面。
- 重新执行故障指令: 缺页中断处理完成后,被中断的指令会被重新执行,确保程序能够继续执行。
缺页故障的发生通常是因为程序访问了虚拟内存中尚未加载到物理内存的页面,操作系统通过中断机制和相应的处理程序来动态地管理内存,确保程序能够正常运行。
在这个过程中,如果物理内存中没有足够的空间来容纳新的页面,操作系统可能需要选择一个页面进行替换。这个选择过程就是页面调度算法的核心,常见的算法包括最近最少使用(LRU)等。缺页中断处理是虚拟内存系统中关键的一环,它允许程序访问远远超过物理内存容量的虚拟地址空间。
7.9动态存储分配管理
动态内存分配是在程序运行时根据需要分配和释放内存的过程。C语言中,常用的动态内存管理函数是malloc、free、calloc、realloc等。与之相关的是printf函数,尤其在处理可变数量的参数时,可能会调用malloc。
基本方法:
- malloc: 用于分配指定大小的内存块。语法为 void* malloc(size_t size),返回一个指向分配内存的指针。需注意手动释放内存。
- free: 用于释放之前通过malloc等函数分配的内存。语法为 void free(void* ptr),其中ptr是要释放的内存块的指针。
- calloc: 与malloc类似,但会初始化分配的内存块为零。语法为 void* calloc(size_t num, size_t size)。
- realloc: 用于更改之前分配的内存块的大小。语法为 void* realloc(void* ptr, size_t size)。
策略:
- 首次适应(First Fit): 分配时按顺序找到第一个大小足够的空闲块。
- 最佳适应(Best Fit): 分配时找到能满足请求且剩余空间最小的空闲块。
- 最坏适应(Worst Fit): 分配时找到能满足请求且剩余空间最大的空闲块。
- 循环首次适应(Next Fit): 与首次适应类似,但从上次分配结束的地方开始查找。
- 动态内存池: 维护一个池,预先分配一定数量的内存块。通过分配和释放操作管理内存。
- 内存池缓存策略: 使用LRU(最近最少使用)等算法来决定释放哪些内存块。
动态内存分配的合理使用和释放是避免内存泄漏和提高程序效率的关键。printf函数等涉及动态内存的函数内部会进行内存分配,因此在使用这些函数时需要注意及时释放分配的内存,以免造成资源浪费或内存泄漏。
7.10本章小结
本章详细阐述了hello进程在执行过程中虚拟内存与物理内存之间的关系及相关支持机制。探讨了硬件和软件层面的机制,包括了虚拟内存到物理内存的转换过程。特别关注了缺页异常的处理,系统在异常发生时如何响应。最后,章节深入介绍了动态内存分配的重要作用,并概述了相关的方法与策略。这一系列内容全面展示了操作系统中关键的内存管理机制和动态内存分配的原理,为读者提供了深入理解计算机系统内存管理的基础。
(第7章 2****分)
**
**
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
在Linux中,IO设备管理采用了文件模型和Unix IO接口。通过将每个IO设备抽象为一个文件,实现了设备的模型化,使得不同类型的设备可以通过文件系统进行一致的访问和管理。Unix IO接口提供了标准的系统调用,如open
、read
、write
,使应用程序能够通过统一的方式与各种设备进行通信。这种设计不仅简化了应用程序与底层设备的交互,而且提高了系统的可移植性和扩展性。通过文件模型,IO设备的管理与其他文件类型操作方式相似,使得开发者能够以统一的接口处理各种IO设备,从而促进了系统的效率和灵活性。整体而言,Linux的IO设备管理方法通过这些特征为开发者提供了方便、一致的设备访问接口。
8.2 简述Unix IO接口及其函数
Unix IO(Input/Output)接口是Unix-like操作系统中用于进行输入和输出操作的标准接口。这一接口提供了一组系统调用和函数,用于管理文件、套接字以及其他IO设备。以下是一些常见的Unix IO接口函数:
- open(): 用于打开文件或创建新文件,并返回一个文件描述符。
- close(): 用于关闭一个打开的文件描述符,释放相关的资源。
- read(): 从文件描述符中读取数据。
- write(): 将数据写入文件描述符指定的文件或设备。
- lseek(): 用于在文件中移动文件指针,通常用于随机访问文件。
- fcntl(): 用于对文件描述符进行控制操作,例如修改文件状态标志。
- ioctl(): 用于对设备进行I/O控制操作,通常与设备驱动程序交互。
- select(): 用于异步I/O操作,监视一组文件描述符的状态变化。
- dup() / dup2(): 复制文件描述符,使得多个文件描述符可以引用同一个打开的文件。
- pipe(): 创建一个管道,用于进程间通信。
这些函数构成了Unix IO接口的基础,允许开发者以统一的方式进行文件和设备的读写操作。在Unix-like系统中,这一接口被广泛应用于各种应用程序和系统服务的开发。
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);
- }
在printf的实现中,首先,通过vsprintf将格式化后的字符串存储在缓冲区中。vsprintf函数实现了格式化输出,例如处理%x表示的16进制格式。然后,通过write系统调用将格式化后的字符串写入终端。
在write系统调用中,通过int 0x80或者syscall触发系统调用。在操作系统内部,sys_call函数处理系统调用的分发。在这个例子中,sys_call通过调用put_console函数来输出字符到终端。put_console函数中处理了缓冲区溢出问题,并在遇到换行符时输出整行字符串。
总体而言,printf的实现是通过vsprintf格式化字符串,然后通过write系统调用将其写入终端。在底层,字符的显示由显示芯片负责,按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每个点的RGB颜色信息。这整个过程涉及到系统调用、字符显示驱动、格式化输出等多个层面的实现。
8.4 getchar的实现分析
getchar函数是C语言标准库中用于从标准输入(通常是键盘)获取字符的函数。其实现涉及多个层面的处理。首先,当用户按下键盘时,触发异步异常即键盘中断,键盘中断处理程序将按键扫描码转换成ASCII码并存储到系统的键盘缓冲区,以确保及时响应用户输入。在getchar函数中,通过调用read系统函数实现对键盘输入的读取。read系统调用通常会等待用户输入,直到接收到回车键才返回,保证获取完整的用户输入。
getchar函数本身是一个阻塞函数,它等待用户输入,然后调用read函数从键盘缓冲区中读取一个字符,直到检测到回车键,表示输入结束。此时,getchar返回用户输入的第一个字符的ASCII码。如果输入结束或者发生失败,getchar会返回EOF(-1),表示文件结束符。
getchar只能读取字符型输入,其等待方式是用户按下回车键,然后从缓冲区中提取字符。输入的结束方式是回车键,而空格符不会导致结束。这种设计使得getchar在等待用户输入时能够接受空格,但只有在按下回车键后才会返回。此外,getchar以回车键结束输入时会舍弃最后的回车符,确保返回的是用户输入的第一个字符。
8.5本章小结
本章深入探讨了Linux系统中I/O设备的基本概念和管理方法,以及涉及到的键盘输入与输出函数printf和getchar的实现机制。通过分析printf的底层逻辑、系统调用以及字符显示驱动,加深了对I/O操作的理解。getchar函数的阐述涉及异步异常处理、系统调用read和字符输入缓冲区,窥探了其在等待用户输入时的内部工作原理。这些知识为理解Linux系统底层运作提供了重要的基础。
(第8章1分)
结论
Hello程序的实现经历了多个关键步骤,反映了计算机系统设计与实现的复杂性。首先,程序员通过文本编辑器编写了最初的hello.c源代码。接着,预处理器展开了代码,处理#include和#define等,生成了预处理后的源代码hello.i。编译器将C语言代码翻译成汇编语言代码hello.s,进入了机器层面。然后,汇编器将汇编代码转换为可重定位目标文件hello.o,这一步将使用的绝对地址暂时保留为重定位条目。
进入链接阶段,链接器将hello.o与必要的库链接并进行重定位,得到可执行文件hello。这个阶段将不同目标文件合并成一个整体,确保函数调用、变量引用等都能正确连接。
在程序运行时,shell通过fork创建进程,使用execve加载可执行文件hello及其动态链接库,通过虚拟内存机制将节映射到内存空间。此时,hello进程可能产生各种异常与信号,如键盘中断、SIGTSTP、SIGINT等。
在Hello的执行过程中,它使用了一个独立的虚拟地址空间,通过分段机制和分页机制进行内存访问。同时,Hello需要通过中断与IO端口等方式与外部硬件设备进行交互,这牵涉到底层的输入输出操作。
程序的执行也牵扯到对I/O设备的操作,包括printf和getchar函数的调用。这与Linux系统的I/O设备管理密切相关,涉及键盘中断处理、按键扫描码转ASCII码等操作。
最终,Hello在CPU流水线中执行每一条指令,运行结束后,父进程回收并将其从系统中清除。Hello程序的一生虽然看似简单,实则涵盖了编译、链接、进程创建、内存管理、异常处理、I/O设备交互等多个计算机系统的重要概念。对计算机系统的深刻理解和优化理念需要关注准确性和性能的平衡,同时掌握系统调用、中断处理等关键技术,方能构建高效可靠的系统基础。这一过程是对计算机系统设计与实现的全面认识,为今后的系统优化和创新奠定了基础。
(结论0分,缺失 -1****分,根据内容酌情加分)
**
**
附件
中间结果文件名称 | 文件作用 |
---|---|
hello | 可执行目标程序 |
hello.elf | hello的elf格式文件 |
hello.i | 预处理后的文件 |
hello.o | 可重定位目标文件 |
hello.s | 汇编程序 |
hello_o.elf | hello.o的elf格式文件 |
hello_o_obj.txt | hello.o的反汇编代码 |
hello_obj.txt | hello的反汇编代码 |
(附件0分,缺失 -1****分)
**
**
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 伍之昂. Linux Shell 编程从初学到精通 [M]. 北京:电子工业出版社
[2] Randal E,Brynant, David R. O’Hallaron. 深入理解计算机系统(原书第三版). 北京:机械工业出版社,2016.
[3] Stallings.计算机组成与体系结构:性能设计(原书第8版). 北京:机械工业出版社,2011.
[4] https://www.cnblogs.com/pianist/p/3315801.html
[5] https://blog.csdn.net/m0_65601072/article/details/124650579
[6] https://www.cnblogs.com/knife-king/p/11090029.html
[7] https://www.elecfans.com/emb/20190402898901.html
结论
Hello程序的实现经历了多个关键步骤,反映了计算机系统设计与实现的复杂性。首先,程序员通过文本编辑器编写了最初的hello.c源代码。接着,预处理器展开了代码,处理#include和#define等,生成了预处理后的源代码hello.i。编译器将C语言代码翻译成汇编语言代码hello.s,进入了机器层面。然后,汇编器将汇编代码转换为可重定位目标文件hello.o,这一步将使用的绝对地址暂时保留为重定位条目。
进入链接阶段,链接器将hello.o与必要的库链接并进行重定位,得到可执行文件hello。这个阶段将不同目标文件合并成一个整体,确保函数调用、变量引用等都能正确连接。
在程序运行时,shell通过fork创建进程,使用execve加载可执行文件hello及其动态链接库,通过虚拟内存机制将节映射到内存空间。此时,hello进程可能产生各种异常与信号,如键盘中断、SIGTSTP、SIGINT等。
在Hello的执行过程中,它使用了一个独立的虚拟地址空间,通过分段机制和分页机制进行内存访问。同时,Hello需要通过中断与IO端口等方式与外部硬件设备进行交互,这牵涉到底层的输入输出操作。
程序的执行也牵扯到对I/O设备的操作,包括printf和getchar函数的调用。这与Linux系统的I/O设备管理密切相关,涉及键盘中断处理、按键扫描码转ASCII码等操作。
最终,Hello在CPU流水线中执行每一条指令,运行结束后,父进程回收并将其从系统中清除。Hello程序的一生虽然看似简单,实则涵盖了编译、链接、进程创建、内存管理、异常处理、I/O设备交互等多个计算机系统的重要概念。对计算机系统的深刻理解和优化理念需要关注准确性和性能的平衡,同时掌握系统调用、中断处理等关键技术,方能构建高效可靠的系统基础。这一过程是对计算机系统设计与实现的全面认识,为今后的系统优化和创新奠定了基础。
(结论0分,缺失 -1****分,根据内容酌情加分)
**
**
附件
中间结果文件名称 | 文件作用 |
---|---|
hello | 可执行目标程序 |
hello.elf | hello的elf格式文件 |
hello.i | 预处理后的文件 |
hello.o | 可重定位目标文件 |
hello.s | 汇编程序 |
hello_o.elf | hello.o的elf格式文件 |
hello_o_obj.txt | hello.o的反汇编代码 |
hello_obj.txt | hello的反汇编代码 |
(附件0分,缺失 -1****分)
**
**
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 伍之昂. Linux Shell 编程从初学到精通 [M]. 北京:电子工业出版社
[2] Randal E,Brynant, David R. O’Hallaron. 深入理解计算机系统(原书第三版). 北京:机械工业出版社,2016.
[3] Stallings.计算机组成与体系结构:性能设计(原书第8版). 北京:机械工业出版社,2011.
[4] https://www.cnblogs.com/pianist/p/3315801.html
[5] https://blog.csdn.net/m0_65601072/article/details/124650579
[6] https://www.cnblogs.com/knife-king/p/11090029.html
[7] https://www.elecfans.com/emb/20190402898901.html
(参考文献0分,缺失 -1****分)