hello的一生

摘 要

本文从经典教例hello出发,以hello教例的编译与运行过程为线索,讨论并分析了在hello执行过程中,计算机系统进行的预处理、编译、汇编、链接、进程管理、存储管理、IO管理等操作,完成了从源代码到程序,从程序到进程(P2P),从0到0(020)的全流程。

关键词:Linux;操作系统;计算机科学;进程管理;存储管理

第1章 概述

1.1 Hello简介

受K&R所著《C程序设计语言》的影响,几乎所有的编程的教程都以教读者如何使用对应的编程语言、编程工具在程序中输出“hello,world”为开端。也正因如此,几乎所有的程序员,以及计算机相关行业的从业人士,其专业的开端,都离不开对这句“hello,world”的接触与实现。毋庸置疑,“hello,world”这个教例(下称“Hello”或“hello”)在事实上成为了现当代计算机文化中不可分割的一部分。[1]我们现在有许多其他的教例,由于包含“hello”字样的输出,因而也可以称为“hello”。他们本质上是程序员对一种新技术的最原始接触。

作为计算机专业知识的起点,hello的实现虽然简单,但是却可以折射出现代计算机系统的基本运作规律。对于大部分人而言,hello是第一个让他们产生“从程序到进程”(From Program to Process, P2P)概念的程序,同样是第一个“从0到0”(From Zero-0 to Zero-0, 020)的程序。

我们可以较为具体地描述P2P和020如下:

P2P是指在操作系统中,hello的完成过程由源代码开始,经过预处理器处理为修改了的源程序,然后经编译器处理为汇编程序,再经过汇编器处理为可重定位目标程序,最后经链接器与其他的可重定位目标程序相链接,得到我们最后的目标程序。(实际上链接器的工作并不只是在编译时将可重定位目标程序之间相链接。比如,考虑动态链接器的工作:动态链接器在程序的运行时,负责加载动态链接库中的函数。)这是一个由源代码文件,经过进程处理到程序的过程。

操作系统内核接受到用户的命令,或者执行其内在的逻辑,加载程序。内核通过与进程管理系统与虚拟内存系统通讯,将产生一个新的子进程,该子进程在缺省的情况下会以调用fork()函数产生之的那个进程为父进程;同时,虚拟内存系统为这个子进程创建虚拟内存区域。以在shell中执行hello为例:不考虑对错误语义与语法的特殊处理的情况下,shell若判别输入的命令不是一个内置指令,则会先fork()产生一个子进程,然后通过execve加载并执行我们指定的hello可执行程序。内核为hello映射虚拟内存,依某种策略(取决于内核的具体实现,内核可能不会第一时间装入整个hello程序)将hello装入虚拟内存,并为运行着的hello分配时间片以执行逻辑控制流。这是一个由程序文件到进程的过程。即P2P。

020则是从hello(或者其他进程)装入虚拟内存到资源被释放的过程。0意指在进程产生的之前与之后,操作系统中没有关于该进程的数据(除却该进程对应的程序文件以及其IO结果)。

020从内核与存储管理系统的交互开始。内核向存储管理系统发出请求,将目标文件读入主存。这一过程中,硬件实现先向cache发出请求,如cache有未命中的数据,则再向下一层cache申请数据,在存储器层次结构中逐级向下,直到确认没有存储器带有目标数据,或者成功调取目标数据。内核为hello进行虚拟内存的分配,进行内存映射。hello在这个过程以后被加载到主存中。

程序计数器PC随之发生变化,hello的可执行文件中的指令和数据被堵住内存。CPU以流水线形式读取并执行指令,执行逻辑控制流。内核进行进程调度,为进程分配时间片,进行上下文切换。

父进程退出而子进程还在运行的情况下,这些子进程成为孤儿进程,并将为init进程所收养,init进程完成它们的状态收集工作。子进程退出而父进程还在运行的情况下,进程变为僵死进程(或称“僵尸进程”),此时子进程的进程描述符还保存在系统中,需要父进程通过wait()或者waitpid()获取子进程的状态信息。如果父进程不调用wait()或者waitpid()的话,那么保留的信息不会被释放,其进程号会被一直占用。[2]

父进程通过wait()或者waitpid()获取了子进程的状态信息以后,子进程的资源被释放。内核删除与该进程相关的数据对象。

1.2 环境与工具

编写本论文过程中,我所使用的软硬件环境如下:

❯ neofetch --off

OS: Arch Linux x86_64

Host: 82L5 Lenovo XiaoXinPro 16ACH 2021

Kernel: 6.9.1-zen1-1-zen

Uptime: 19 hours, 43 mins

Packages: 3405 (pacman), 7 (flatpak)

Shell: zsh 5.9

Resolution: 2560x1600

DE: Plasma 6.0.4

WM: kwin

Theme: [Plasma], Materia [GTK2/3]

Icons: [Plasma], breeze-dark [GTK2/3]

Terminal: yakuake

CPU: AMD Ryzen 7 5800H with Radeon Graphics (16) @ 4.463GHz

GPU: AMD ATI Radeon Vega Series / Radeon Vega Mobile Series

我所使用的开发环境如下:

❯ gcc -v

gcc -v

使用内建 specs。

COLLECT_GCC=gcc

COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/14.1.1/lto-wrapper

目标:x86_64-pc-linux-gnu

配置为:/build/gcc/src/gcc/configure --enable-languages=ada,c,c++,d,fortran,go,lto,m2,objc,obj-c++,rust --enable-bootstrap --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://gitlab.archlinux.org/archlinux/packaging/packages/gcc/-/issues --with-build-config=bootstrap-lto --with-linker-hash-style=gnu --with-system-zlib --enable-__cxa_atexit --enable-cet=auto --enable-checking=release --enable-clocale=gnu --enable-default-pie --enable-default-ssp --enable-gnu-indirect-function --enable-gnu-unique-object --enable-libstdcxx-backtrace --enable-link-serialization=1 --enable-linker-build-id --enable-lto --enable-multilib --enable-plugin --enable-shared --enable-threads=posix --disable-libssp --disable-libstdcxx-pch --disable-werror

线程模型:posix

支持的 LTO 压缩算法:zlib zstd

gcc 版本 14.1.1 20240507 (GCC)

❯ nvim -v

nvim -v

NVIM v0.10.0

Build type: Release

LuaJIT 2.1.1713773202

❯ make -v

make -v

GNU Make 4.4.1

为 x86_64-pc-linux-gnu 编译

❯ cmake --version

cmake --version

cmake version 3.29.3

❯ gdb -v

gdb -v

GNU gdb (GDB) 14.2

1.3 中间结果

本论文使用的源代码文件为hello.c,目标二进制文件为hello。

在编译的过程中生成的中间结果文件如下:

  • hello.i:修改了的源程序。是预处理器对源代码进行预处理以后得到的文本文件,其中已经完成了对头文件的包含、宏扩展等工作。

  • hello.s:汇编程序。是编译器根据hello.i生成的汇编代码文本。需要经过汇编器生成二进制程序以后方可执行。该程序包含函数main()的定义。

  • hello.o:可重定位目标程序。由汇编器翻译得到,是二进制程序,其内是机器语言指令。经过链接可以得到目标的二进制文件。

  • hello:汇编并链接得到的可执行程序文件。该程序可以直接执行,是hello.c的直接编译结果。

  • das_hello.s:通过objdump对hello.o进行反汇编的结果。

  • das_hello_exec.s:通过objdump对hello进行反汇编的结果。

  • hello_d:动态链接的hello。

1.4 本章小结

本章主要介绍了hello这一教例的来源与教育意义,引入了P2P与020的概念。简单介绍了hello程序的P2P过程和020过程,并给出了撰写本论文时使用的软硬件环境与生成的中间结果。

第2章 预处理

2.1 预处理的概念与作用

一些程序设计语言有一个叫预处理的翻译阶段。

预处理由预处理器执行。C预处理器用于在编译器ccl汇编程序之前生成修改了的源程序。

C语言标准规定,预处理是指前四(或前八)个编译阶段。[3]预处理对应的四个编译阶段分别为:

  1. 三字符组与双字符组被替换为单字符的内部表示。

  2. 进行拼接,把物理源码行处理为逻辑行的顺序集合。

  3. 单词化,将源文件分解为预处理标记和空白字符序列。

  4. 执行预处理指令,展开宏调用,并执行_Pragma一元运算符表达式。[4]

就预处理器的作用而言,常用的预处理器指令有#include、#define,它们完成头文件的包含、宏扩展等工作。#pragma会被保留——它将被提供给编译器。

严格来说,去除注释也是预处理器工作的一环。

2.2在Linux下预处理的命令

在Linux下进行预处理的命令为gcc -E hello.c -o hello.i。该命令等价于cpp hello.c > hello.i,生成预处理后的源代码文件hello.i。-E参数提示gcc仅需预处理程序。而cpp直接将输入其中的源代码进行预处理并输出到标准输出。


图1 : 在Linux下执行对hello.c的预处理(画面右侧是hello.i的内容)


 

2.3 Hello的预处理结果解析

main函数之前出现了大量的函数定义、结构体定义、类型别名,以及形同“# 1 "/usr/include/stdio.h" 1 3 4”的行标记。这些函数定义、结构体定义、类型别名来自于头文件的包含,而以#开头带有路径和数字的语句称为行标记(linemarker),其作用时用于识别特定代码行来自于哪个源文件和行,它们可以被用于生成更准确的诊断信息。[4–7]

来自于头文件的包含的代码,带有大量的以下划线为开头的数据对象。以下划线开头进行命名的数据对象一般是被保留给实现的。[8]


图2 : hello.i的内容

2.4 本章小结

一些程序语言设计有名为预处理的编译阶段,用于在编译器ccl汇编程序之前生成修改了的源程序,以实现头文件的包含等操作。我们用gcc的-E选项指定gcc仅预处理我们的源代码文件。拆视hello的预处理结果,我们注意到其中出现了大量的新的C语句以及一些被称为行标记的数据,前者来自于头文件,后者则生成自预处理器,目的是识别特定代码行来自于哪个源文件和行。

第3章 编译

3.1 编译的概念与作用

编译器是把源代码转换成可执行代码的程序。[8]。

编译器ccl将文本文件hello.i翻译成文本文件hello.s,hello.s包含了一个汇编语言程序。这个汇编语言程序包含了函数main的定义。[9]

就最一般的情况而言,一台符合冯·诺伊曼架构的计算机至少包含了一个中央处理器。对于编译器而言,编译器要根据中央处理器的架构决定所使用的汇编语言(不同架构的处理器所使用的机器语言以及与机器语言对应的汇编语言往往有一定的区别,这取决于对应指令集的情况)。这是由于处理器只能识别作为二进制数据的机器语言。编译器的工作是把预处理过的源代码hello.i转换成汇编程序hello.s,以方便接下来汇编工作的开展。这是由C语言代码以及行标记生成汇编语言程序的过程。

汇编语言是机器代码的文本表示,是对机器语言的一种抽象。

3.2 在Linux下编译的命令


图3 : 在Linux下对hello.c进行编译(画面右侧为编译结果hello.s的内容)


我们通过gcc -S hello.i -o -hello.s或者gcc -S hello.c -o hello.s来得到hello的汇编代码。-S参数提示gcc仅需编译程序得到汇编代码。

3.3 Hello的编译结果解析


图4 : hello.c的内容

观察hello的源代码,我们可以注意到,main()通过int argc与char *argv[]接受来自于命令行的参数。第11行的int i作为main()函数中的局部变量存在。

而后通过argc判断程序参数的个数。如若参数个数错误,则输出用法信息并结束进程;否则通过for循环,以给定秒数为间隔输出10次Hello和输入的参数——程序最后读入一个输入方退出。

3.3.1 常量

程序包含数个常量。

其中有用于在标准输出中产生输出的字符串字面值常量,也有用于逻辑判断与程序控制的数值字面值常量。


图5 : 用于输出的字符串字面值常量


上图的汇编语言代码分别对应:

  • "用法: Hello 学号 姓名 手机号 秒数!\n"

  • "Hello %s %s %s\n",argv[1],argv[2],argv[3]

这两个字面值常量将以只读数据段的形式被编入可重定位目标文件。并作为printf输出时候的格式段。

数值字面值常量的存在则以立即数为主。


图6 : 数值字面值常量(用于判断argc是否等于5的立即数)


图7 : 数值字面值常量(用于给for循环中的变量i赋值为0的立即数)


图8 : 数值字面值常量(作为for循环控制循环条件为i<10的立即数)


以上三图中的立即数分别用于判断argc是否等于5、给for循环构建i=0的初始条件、给for循环构建i<10的循环条件。值得注意的是,编译器在生成汇编代码的时候对i<10的条件进行了等价的转换,转换为了i<=9,所以图8中汇编代码相对行号为13的行中立即数为9。

总而言之,编译器将字符串的字面量常量(在此同时也是printf使用的格式串)存储在只读数据。将数值字面值常量则以立即数的形式写入汇编代码中。涉及到条件判断的情况可能会将数值字面值常量作等价改写。

3.3.2 变量

程序中有三个变量:argc、argv和i。

其中,argc和argv是main()函数的形式参数,由外界传入。而i定义在main()的函数体内部。


图9 : main函数入口处的内存分配与%rbp的保存

我们可以注意到在函数入口处被标记为.LFB6的代码段中,程序通过pushq的方式保存了寄存器rbp中的内容,将其入栈。同时通过subq的操作将栈帧对齐,并为以上的变量分配空间。紧随其后的movl、movq与cmpl揭示了,形式参数argc一开始存放在寄存器edi中,而后被数据传送到地址%rbp-20处;形式参数argv一开始存放在寄存器rsi中,而后被数据传送到地址%rbp-32处。edi和rsi恰为用于存放当前函数第1个参数和第2个参数的寄存器。[9]


图10 : L2段的代码,这应该是for循环的初始化阶段


图11 : L3段的代码,图中相对行号为25处为for循环条件


 

我们可以注意到:用于初始化循环以及作为循环判断条件的变量均存储在%rbp-4这一内存地址处。结合程序之前的行为,可以得出%rbp-4处为变量i所在。

3.3.3 类型

通过程序的源代码,我们判断该程序没有声明类型别名和结构体,也没有使用类似于联合的特性与结构。程序仅在main()函数的定义以及局部变量i的定义中使用了类型。

通过对汇编代码的阅读,我们注意到除却main函数入口处对栈指针rsp的操作以外,没有任何显式的成模式的有关于内存分配的描述与语句出现。变量i是直接被当作%rbp-4中的数据处理的,直接地被认为是存储在了%rbp-4处。而rbp存储的是rsp移动之前的值,可以认为是当前栈帧地址的一个上界。

由此我们可以推定,类型在编译阶段的处理就是用于确定数据对象对于栈指针以及rbp寄存器中地址的偏移。这种偏移的计算还要经过栈帧的对齐——%rbp-20与%rbp-4之间有16个字节的空间,但是这个空间没有得到充分的利用,%rbp-20处存储的是与%rbp-4处一样的int类型变量,所占的字节仅有4字节。中间有12个字节的空缺。这应当是由栈指针的对齐操作引起的。作用于栈指针rsp的subq使用的操作数为32。这个32得自于main()函数体中的数据对象i与形式参数argc与argv,这些数据对象的类型提示了它们所需的内存空间;这一内存空间与栈指针对齐的需求相结合,最后计算得到了32这一操作数。

(%rbp-24到%rbp-20处的地址空间同样有4字节的空缺。%rbp-32到%rbp-23中间的八个字节里仅用于存储argv数组的地址。)

这一过程中类型并没有直接作用于我们的汇编代码,没有任何一行汇编代码提及类型。(C语言的)类型通过影响堆栈中的内存分配来生效。

3.3.4 表达式和关系操作

程序中带有逻辑表达式argc!=5和i<10,以及算术表达式i++。对格式串的讨论我们已经在前文的常量部分讨论完毕;对算术表达式i++的讨论我们将会放在下文的算术操作中进行。本节我们主要讨论的是编译过程对argc!=5和i<10这两个逻辑表达式的处理。同时,有关于这两个逻辑表达式的流程控制我们将放在下文讨论。


图12 : i<10的判断表达式

文图6的第23行,以及上图12的第57行,分别是我们对逻辑表达式argc!=5与i<10的实现。我们可以注意到两个表达式都是通过cmpl这一指令实现的。cmpl是对双字型数据的比较,它会根据两个操作数之差设置(由“后者-前者”的结果设置)条件码。条件码为下文的跳转指令提供了判断依据。

逻辑判断就程序语言设计的原语角度而言,是一个二元的操作,其包含了两个操作数。编译器将对应的操作数给到cmpl,并通过jle进行代码跳转,进行逻辑判断工作。

我们可以注意到编译器将i<10的逻辑表达式等效地替换为了i<=9。这体现出,编译器会出于程序运行效率考虑,对一些计算操作进行优化。

3.3.5 算术操作(++)


图13 : for循环的循环体,图中行号为55处为i++操作对应的汇编语句


序中设计到的唯一的算数操作就是作为for循环进行递增的i++。

程序通过addl指令实现i++,通过这一指令,使%rbp-4处的数据加1。这个1以立即数的形式出现。

编译器将自增操作(++)处理为操作数之一为1的add指令。

3.3.6 未初始化的定义(不赋初值)

我们前文已经在“类型”一节里已经讨论过了:“类型在编译阶段的处理就是用于确定数据对象对于栈指针以及rbp寄存器中地址的偏移。”

对于未初始化的定义,编译器的处理是为其确定地址偏移——分配内存。但是不为其初始化。也就是不通过数据传送指令或者加载有效地址指令为分配的内存赋值。

3.3.7 函数的调用和返回与参数传递

在我们的控制流中,我们调用的函数包括exit()和pritnf(),以及sleep()和atoi()。我们给exit()传入参数0,给printf()传入输出的串。同时我们在main()函数的最后通过return返回。sleep()为for循环循环体的执行提供一个时间间隔,而atoi()则用于处理输入给sleep()的参数。程序在返回前还调用了一个getchar(),这是用于使得程序在用户输入任意按键以后再结束。

编译器将函数调用转化为call指令,以运行printf()和exit()等函数。其中源代码第15行的printf()由于以\n结尾,而被编译器优化为puts。


图14 : 汇编代码中的call指令,每一个call指令都意味着一次函数的调用


在x86-64机器中,将控制从一个函数转移到另一个函数,只需要简单调整一下程序计数器的起始位置。不过由于这种控制的转移不能影响从被调用者函数返回以后调用者函数的运行,所以call指令的执行一般会伴随一个返回地址的压入。这个返回地址指向被调用者函数返回以后程序计数器PC应当指向的位置。[9]

我们可以注意到在程序调用前,部分的函数有对$rdi或者$edi进行数据传送的操作。这其实是在进行被调用者函数参数的传递。数据传送是简单的复制操作,这同样解释了为什么“按指针传递”的参数传递方式能让被调用的函数影响作为调用者的函数中的数据对象,而“按值传递”不行。

(仅讨论64位寄存器的情况下,)函数通过%rdi、%rsi、%rdx、%rcx、%r8和%r9这样几个寄存器,传递函数的第一到六个参数。[9]寄存器组是被所有过程共享的资源。[9]如果寄存器组中的六个用于存储参数的寄存器不够用,即有一个函数有大于6个的参数,那么超出6个的部分就会被通过栈的形式来传递。即在call转移控制时要先将对应的数据拷贝到被调用者函数即将分配的栈帧的对应位置。被调用者函数要划分一块名为“参数构造区”的内存给这些通过栈传递的参数。[9]


图15 : hello中的返回操作


数的返回由ret汇编指令进行。这一指令同样存在在前述的被调用者函数中。正如call同时完成返回地址入栈和跳转的工作,ret同时完成返回地址出站和跳转的工作。执行ret以后,原先被压入栈中的返回地址会弹出栈帧。程序会根据弹出的返回地址设置程序计数器PC,以跳转到原函数调用此函数位置的下一语句对应的地址处。

值得一题的是,返回地址存储在栈帧中,这是缓冲区溢出攻击攻击的目标之一。未经保护的返回地址很容易被用于劫持程序的控制流。

3.3.8 数组操作


图16 : L4程序段中被传递给printf的argv[1]、argv[2]和argv[3]

通过对源程序第19行的printf()函数调用(图中对应的汇编语言代码为相对行数14处)前的参数传递进行分析,我们得到结论:编译器为处理数组下标,将寄存器中的argv数组地址取出到rax寄存器中。然后通过addq指令对rax寄存器中的地址施加偏移,进而得到argv[1]、argv[2]、argv[3]这些数组元素的地址。这些地址被传递入printf作为参数。

即,编译器通过数组地址加上偏移的方式处理数组下标的计算。

3.3.9 控制转移

程序中所涉及到的控制转移涵盖if语句与for循环,以及函数的调用。函数的调用与返回在前文已经讨论。

if语句的实现见汇编代码第23和24行。

i


图17 : hello中的if语句


f语句的实现是通过cmpl指令和条件跳转指令je实现的。je在条件码判断两个操作数相等的时候进行跳转。.L2的程序段包含的是for循环的初始化。这里的操作其实是计算%rbp-20与立即数5的差值,得到的结果如果是0(也就是argc等于5),则跳转.L2,亦即进行for循环,否则执行if的语句中的代码块。

for循环的控制与此类似:它包含一个简单的初始化流程——对用于控制循环的变量进行赋值,然后执行和if语句相近的循环条件判断——同样是通过cmpl语句和jmp语句(这里用的是jle)进行条件判断;循环体的最后则是循环变量的自增操作,自增操作的实现在上文已经讨论。

程序通过条件跳转实现其内的分支判断与循环。

3.4 本章小结

本章中我们回顾了编译的定义与概念。编译是通过源代码得到汇编语言代码的过程。我们通过-S参数使gcc编译器进行仅编译的操作。

我们剖析了hello的编译结果,针对汇编代码中对源程序中常量、变量、类型等C语言特性的实现作了分析与讨论。

第4章 汇编

4.1 汇编的概念与作用

汇编是由汇编代码生成机器语言的过程。

汇编由汇编器as执行,汇编器as读入汇编程序hello.s,生成可重定位目标程序hello.o。hello.o是一个二进制文件,使用文本编辑器查看它会得到一堆乱码。

4.2 在Linux下汇编的命令


图18 : 使用vim查看hello.o文件


图19 : 编译并查看hello.o


 

在Linux下对hello进行汇编的指令是gcc -c hello.c -o hello.o我们通过-c程序提示gcc仅需要进行汇编。hello.c可以为得到hello.o之前的中间结果文件所替代(比如hello.s)。

4.3 可重定位目标elf格式

❯ readelf -a hello.o

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: 1080 (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

节头:

[号] 名称 类型 地址 偏移量

大小 全体大小 旗标 链接 信息 对齐

[ 0] NULL 0000000000000000 00000000

0000000000000000 0000000000000000 0 0 0

[ 1] .text PROGBITS 0000000000000000 00000040

000000000000009f 0000000000000000 AX 0 0 1

[ 2] .rela.text RELA 0000000000000000 000002e8

00000000000000c0 0000000000000018 I 11 1 8

[ 3] .data PROGBITS 0000000000000000 000000df

0000000000000000 0000000000000000 WA 0 0 1

[ 4] .bss NOBITS 0000000000000000 000000df

0000000000000000 0000000000000000 WA 0 0 1

[ 5] .rodata PROGBITS 0000000000000000 000000e0

0000000000000040 0000000000000000 A 0 0 8

[ 6] .comment PROGBITS 0000000000000000 00000120

000000000000001c 0000000000000001 MS 0 0 1

[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000013c

0000000000000000 0000000000000000 0 0 1

[ 8] .note.gnu.pr[...] NOTE 0000000000000000 00000140

0000000000000030 0000000000000000 A 0 0 8

[ 9] .eh_frame PROGBITS 0000000000000000 00000170

0000000000000038 0000000000000000 A 0 0 8

[10] .rela.eh_frame RELA 0000000000000000 000003a8

0000000000000018 0000000000000018 I 11 9 8

[11] .symtab SYMTAB 0000000000000000 000001a8

0000000000000108 0000000000000018 12 4 8

[12] .strtab STRTAB 0000000000000000 000002b0

0000000000000032 0000000000000000 0 0 1

[13] .shstrtab STRTAB 0000000000000000 000003c0

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)

There are no section groups in this file.

本文件中没有程序头。

There is no dynamic section in this file.

重定位节 '.rela.text' at offset 0x2e8 contains 8 entries:

偏移量 信息 类型 符号值 符号名称 + 加数

000000000018 000300000002 R_X86_64_PC32 0000000000000000 .rodata - 4

000000000020 000500000004 R_X86_64_PLT32 0000000000000000 puts - 4

00000000002a 000600000004 R_X86_64_PLT32 0000000000000000 exit - 4

00000000005e 000300000002 R_X86_64_PC32 0000000000000000 .rodata + 2c

00000000006b 000700000004 R_X86_64_PLT32 0000000000000000 printf - 4

00000000007e 000800000004 R_X86_64_PLT32 0000000000000000 atoi - 4

000000000085 000900000004 R_X86_64_PLT32 0000000000000000 sleep - 4

000000000094 000a00000004 R_X86_64_PLT32 0000000000000000 getchar - 4

重定位节 '.rela.eh_frame' at offset 0x3a8 contains 1 entry:

偏移量 信息 类型 符号值 符号名称 + 加数

000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

No processor specific unwind information to decode

Symbol table '.symtab' contains 11 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

4: 0000000000000000 159 FUNC GLOBAL DEFAULT 1 main

5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts

6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit

7: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf

8: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi

9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep

10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar

No version information found in this file.

Displaying notes found in: .note.gnu.property

所有者 Data size Description

GNU 0x00000020 NT_GNU_PROPERTY_TYPE_0

Properties: x86 ISA used:

x86 feature used: x86

以上是我们通过readelf对hello.o分析得到的文件各节基本信息。

根据上述的命令行输出,我们可以分析得到如下信息:

  1. hello.o是一个使用小端序的ELF64程序,使用的ABI为Unix – System V,系统架构为AMD64,入口点地址为0x0。

  2. 0000000000000000到000000000000003f的部分是ELF头,这一部分信息记录了生成该文件的系统的字的大小和字节顺序。前述第一点的内容就是分析自这里。

  3. 0000000000000040到00000000000000de的部分是.text。这一部分是已编译程序的机器代码。

  4. 00000000000002e8到00000000000003a7的部分是.rela.text。这一部分是一个.text节中位置的列表。当链接器把这个目标文件和其他文件组合的时候需要修改这其中记录的位置。

  5. .data和.bss在本程序中不分配任何空间。它们的头在00000000000000df,但是所占用的空间是0。.data记录的是已初始化的全局和静态C变量,.bss记录的是未初始化的。由于本程序没有全局和静态C变量,所以这两节为空。

  6. 00000000000000e0到000000000000012f的部分是.rodata。这一部分几双了程序中的只读数据,包括传入printf的串。

  7. 0000000000000120到000000000000013b的部分是.commnet。这一部分存放了编译器版本信息。

  8. .note.GNU-stack在本程序中不分配任何空间。它的头在000000000000013c,但占用的空间是0。这个节的主要作用是当.note.GNU-stack节存在并且具有特定的标志时,它表明程序不需要或者不允许有可执行的栈。

  9. 0000000000000140到000000000000016f的部分是.note.gnu.property。
    .note.gnu.property 节有助于实现各种安全加固措施,比如确保栈不可执行(类似于 .note.GNU-stack 节的功能,但提供更灵活的控制),或者标记某些段为只读以防止数据段篡改。

  10. 0000000000000170到00000000000001a7的部分是.eh_frame。这个.eh_frame段中存储着跟函数入栈相关的关键数据。当函数执行入栈指令后,在该段会保存跟入栈指令一一对应的编码数据,这些编码数据可以用于计算出当前函数栈大小和CPU寄存器的入栈情况。

  11. 00000000000003a8到00000000000003bf的部分是.rela.eh_frame。这一部分是一个.eh_frame节中位置的列表。当链接器把这个目标文件和其他文件组合的时候需要修改这其中记录的位置。

  12. 00000000000001a8到00000000000002af的部分是.symtab。这是一个符号表,用来存放在程序中定义和引用的函数和全局变量的信息。它不包含局部变量的条目。

  13. 00000000000002b0到00000000000002e1的部分是.strtab。这是一个字符串表,是以null为结尾的字符串的序列,其内容包括.symtab和.debug中的符号表,以及节头部的节名字。

  14. 00000000000003c0到0000000000000443的部分是.shstrtab。这节负责存储所有其他节的名称字符串。在ELF文件中,每个节都有一个名称,这些名称没有直接存储在节头表中,而是通过索引引用.shstrtab节中的偏移量来间接访问。

4.4 Hello.o的结果解析

通过objdump -d -r hello.o,生成了hello.o的反汇编文件das_hello.s。

机器语言是二进制的格式。我们得到的代码,其右半部分给出了机器语言相应的汇编语言转写。


图20 : das_hello.o文件中的内容


我们可以注意到,在hello.s中,分支跳转是通过je .L2这样的语句执行的,这种跳转通过.L2这样的助记符进行,而das_hello.s中的跳转则是以je 2e<main+0x2e>这样的形式进行——助记符被转化为了内存地址。

call指令也有一定的变化。hello.s中的call形同call pritnf@PLT,而das_hello.s中的call则形同call 6f<main+0x6f>。call后的操作数从函数名变为了对应函数的相对偏移地址。

4.5 本章小结

本章中我们回顾了汇编的定义与概念。汇编是通过汇编语言代码得到机器语言代码的过程。我们通过-c参数使gcc编译器进行汇编的操作。

我们通过objdump剖析了hello的汇编结果,剖析了hello.o的反汇编文件das_hello.s。同时我们解析了ELF格式的可重定位目标文件其组成。

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,也是将一个或者多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件的过程。它可以执行在编译时、加载时,甚至可以执行在运行时。链接由链接器进行。大多数的现代操作系统都提供动态链接和静态链接两种形式。[10]

简而言之,链接器的工作是解析未定义的符号引用,将目标文件的占位符替换为符号的地址。

静态链接是以一组可重定位目标文件和命令行参数为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。链接器在这一个过程中主要完成符号解析和重定位的工作。

动态链接使用共享库(或者说是动态库、动态链接库)进行链接操作。共享库是一个内存模块,在运行和加载的时候可以加载到任意的内存地址,并和内存中的一个程序相链接。可以加载而毋须重定位的代码称为位置无关代码。共享库总是需要被编译为状态无关代码。

5.2 在Linux下链接的命令


图21 : 在Linux下进行链接的演示

我们通过指令ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /lib64/crt1.o /lib64/crti.o /lib64/libc.so.6 /lib64/crtn.o ./hello.o进行我们的链接工作。

-o告诉链接器输出文件的文件名,-dynamic-linker指定我们使用的动态链接目标,其他的参数则告诉链接器应该使用哪些可重定位目标文件进行静态链接。

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

❯ readelf -a 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

入口点地址: 0x401090

程序头起点: 64 (bytes into file)

Start of section headers: 13448 (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: 26

Section header string table index: 25

节头:

[号] 名称 类型 地址 偏移量

大小 全体大小 旗标 链接 信息 对齐

[ 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

0000000000000040 0000000000000000 A 0 0 8

[ 3] .note.ABI-tag NOTE 0000000000400340 00000340

0000000000000020 0000000000000000 A 0 0 4

[ 4] .hash HASH 0000000000400360 00000360

0000000000000038 0000000000000004 A 6 0 8

[ 5] .gnu.hash GNU_HASH 0000000000400398 00000398

000000000000001c 0000000000000000 A 6 0 8

[ 6] .dynsym DYNSYM 00000000004003b8 000003b8

00000000000000d8 0000000000000018 A 7 1 8

[ 7] .dynstr STRTAB 0000000000400490 00000490

0000000000000067 0000000000000000 A 0 0 1

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

0000000000000012 0000000000000002 A 6 0 2

[ 9] .gnu.version_r VERNEED 0000000000400510 00000510

0000000000000030 0000000000000000 A 7 1 8

[10] .rela.dyn RELA 0000000000400540 00000540

0000000000000030 0000000000000018 A 6 0 8

[11] .rela.plt RELA 0000000000400570 00000570

0000000000000090 0000000000000018 AI 6 20 8

[12] .init PROGBITS 0000000000401000 00001000

000000000000001b 0000000000000000 AX 0 0 4

[13] .plt PROGBITS 0000000000401020 00001020

0000000000000070 0000000000000010 AX 0 0 16

[14] .text PROGBITS 0000000000401090 00001090

00000000000000d4 0000000000000000 AX 0 0 16

[15] .fini PROGBITS 0000000000401164 00001164

000000000000000d 0000000000000000 AX 0 0 4

[16] .rodata PROGBITS 0000000000402000 00002000

0000000000000048 0000000000000000 A 0 0 8

[17] .eh_frame PROGBITS 0000000000402048 00002048

0000000000000088 0000000000000000 A 0 0 8

[18] .dynamic DYNAMIC 0000000000403e38 00002e38

00000000000001a0 0000000000000010 WA 7 0 8

[19] .got PROGBITS 0000000000403fd8 00002fd8

0000000000000010 0000000000000008 WA 0 0 8

[20] .got.plt PROGBITS 0000000000403fe8 00002fe8

0000000000000048 0000000000000008 WA 0 0 8

[21] .data PROGBITS 0000000000404030 00003030

0000000000000004 0000000000000000 WA 0 0 1

[22] .comment PROGBITS 0000000000000000 00003034

000000000000001b 0000000000000001 MS 0 0 1

[23] .symtab SYMTAB 0000000000000000 00003050

0000000000000240 0000000000000018 24 5 8

[24] .strtab STRTAB 0000000000000000 00003290

000000000000011d 0000000000000000 0 0 1

[25] .shstrtab STRTAB 0000000000000000 000033ad

00000000000000d8 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

0x0000000000000600 0x0000000000000600 R 0x1000

LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000

0x0000000000000171 0x0000000000000171 R E 0x1000

LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000

0x00000000000000d0 0x00000000000000d0 R 0x1000

LOAD 0x0000000000002e38 0x0000000000403e38 0x0000000000403e38

0x00000000000001fc 0x00000000000001fc RW 0x1000

DYNAMIC 0x0000000000002e38 0x0000000000403e38 0x0000000000403e38

0x00000000000001a0 0x00000000000001a0 RW 0x8

NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300

0x0000000000000040 0x0000000000000040 R 0x8

NOTE 0x0000000000000340 0x0000000000400340 0x0000000000400340

0x0000000000000020 0x0000000000000020 R 0x4

GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300

0x0000000000000040 0x0000000000000040 R 0x8

GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000

0x0000000000000000 0x0000000000000000 RW 0x10

GNU_RELRO 0x0000000000002e38 0x0000000000403e38 0x0000000000403e38

0x00000000000001c8 0x00000000000001c8 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 .text .fini

04 .rodata .eh_frame

05 .dynamic .got .got.plt .data

06 .dynamic

07 .note.gnu.property

08 .note.ABI-tag

09 .note.gnu.property

10

11 .dynamic .got

Dynamic section at offset 0x2e38 contains 21 entries:

标记 类型 名称/值

0x0000000000000001 (NEEDED) 共享库:[libc.so.6]

0x000000000000000c (INIT) 0x401000

0x000000000000000d (FINI) 0x401164

0x0000000000000004 (HASH) 0x400360

0x000000006ffffef5 (GNU_HASH) 0x400398

0x0000000000000005 (STRTAB) 0x400490

0x0000000000000006 (SYMTAB) 0x4003b8

0x000000000000000a (STRSZ) 103 (bytes)

0x000000000000000b (SYMENT) 24 (bytes)

0x0000000000000015 (DEBUG) 0x0

0x0000000000000003 (PLTGOT) 0x403fe8

0x0000000000000002 (PLTRELSZ) 144 (bytes)

0x0000000000000014 (PLTREL) RELA

0x0000000000000017 (JMPREL) 0x400570

0x0000000000000007 (RELA) 0x400540

0x0000000000000008 (RELASZ) 48 (bytes)

0x0000000000000009 (RELAENT) 24 (bytes)

0x000000006ffffffe (VERNEED) 0x400510

0x000000006fffffff (VERNEEDNUM) 1

0x000000006ffffff0 (VERSYM) 0x4004f8

0x0000000000000000 (NULL) 0x0

重定位节 '.rela.dyn' at offset 0x540 contains 2 entries:

偏移量 信息 类型 符号值 符号名称 + 加数

000000403fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0

000000403fe0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

重定位节 '.rela.plt' at offset 0x570 contains 6 entries:

偏移量 信息 类型 符号值 符号名称 + 加数

000000404000 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0

000000404008 000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0

000000404010 000400000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0

000000404018 000600000007 R_X86_64_JUMP_SLO 0000000000000000 atoi@GLIBC_2.2.5 + 0

000000404020 000700000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0

000000404028 000800000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0

No processor specific unwind information to decode

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 24 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 FILE LOCAL DEFAULT ABS

3: 0000000000403e38 0 OBJECT LOCAL DEFAULT 18 _DYNAMIC

4: 0000000000403fe8 0 OBJECT LOCAL DEFAULT 20 _GLOBAL_OFFSET_TABLE_

5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_mai[...]

6: 0000000000404030 0 NOTYPE WEAK DEFAULT 21 data_start

7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5

8: 0000000000404034 0 NOTYPE GLOBAL DEFAULT 21 _edata

9: 0000000000401164 0 FUNC GLOBAL HIDDEN 15 _fini

10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5

11: 0000000000404030 0 NOTYPE GLOBAL DEFAULT 21 __data_start

12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@GLIBC_2.2.5

13: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__

14: 0000000000402000 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used

15: 0000000000404038 0 NOTYPE GLOBAL DEFAULT 21 _end

16: 00000000004010c0 5 FUNC GLOBAL HIDDEN 14 _dl_relocate_sta[...]

17: 0000000000401090 38 FUNC GLOBAL DEFAULT 14 _start

18: 0000000000404034 0 NOTYPE GLOBAL DEFAULT 21 __bss_start

19: 00000000004010c5 159 FUNC GLOBAL DEFAULT 14 main

20: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5

21: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5

22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5

23: 0000000000401000 0 FUNC GLOBAL HIDDEN 12 _init

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 1 ( 33.3%) 25.0%

3 2 ( 66.7%) 100.0%

Version symbols section '.gnu.version' contains 9 entries:

Addr: 0x00000000004004f8 Offset: 0x000004f8 Link: 6 (.dynsym)

000: 0 (*本地*) 2 (GLIBC_2.34) 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)

004: 3 (GLIBC_2.2.5) 1 (*全局*) 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)

008: 3 (GLIBC_2.2.5)

Version needs section '.gnu.version_r' contains 1 entry:

Addr: 0x0000000000400510 Offset: 0x00000510 Link: 7 (.dynstr)

000000: Version: 1 文件:libc.so.6 计数:2

0x0010: Name: GLIBC_2.2.5 标志:无 版本:3

0x0020: Name: GLIBC_2.34 标志:无 版本:2

Displaying notes found in: .note.gnu.property

所有者 Data size Description

GNU 0x00000030 NT_GNU_PROPERTY_TYPE_0

Properties: x86 ISA needed: x86-64-baseline

x86 feature used: x86

x86 ISA used:

Displaying notes found in: .note.ABI-tag

所有者 Data size Description

GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)

以上是我们通过readelf对可执行文件hello分析得到的文件各节基本信息。

我们可以注意到:

  1. 该程序的数据类型,使用的ABI以及版本等信息与hello.o保持了一致。

  2. 注意到一些数据段的地址不再是全0了。比如.text的地址,现在为0000000000401090。

  3. 相较于hello.o,节头数量由14增加到26。

  4. 由于该可执行程序是完全链接的,所以它不再需要.rel节。

其中我们主要讨论以下节头的作用(与hello.o中节头同名的部分,其作用是一样的):

  1. 段头部标:将连续的文件节映射到运行时的内存段。

  2. .init:其中定义了一个小函数名为_init,程序的初始化代码会调用它。

  3. .line:其中包含源文件中的行数信息,用于符号调试,同时描述源程序与机器代码之间的对应关系。

5.4 hello的虚拟地址空间


图22 : 使用edb查看hello

们使用edb对hello进行查看。

在查看的过程中我们注意到,hello的虚拟地址空间是从0x00400000开始的。

过edb的内存区域功能,我们查看了一些主要的程序段的数据情况。


图23 : 对比查看readelf的输出与edb中的数据传输窗口


图24 : hello中.init的数据情况

上图所示为hello中.init节对应的数据段的数据情况。这里包含1b H个字节,起始地址为00401000。是程序运行的起始点。


图25 : hello中.text段的情况

text段以0000000000401090为起始地址,所占内存为d4 H字节。其中的主要内容为程序的机器代码。通过左右界面的对比,我们可以看到对应数据存储的位置和我们左侧的指令显示区中的数据,二进制上完全一致。

对于可执行文件而言,其中的内容就是二进制。其二进制遵循ELF节头指定的内存地址和空间进行存储,可以通过对二进制的直接查看调用查看。

5.5 链接的重定位过程分析

我们通过objdump得到了hello的反汇编文件das_hello_exec.s。我们将其与das_hello.s进行了对比,得出了如下差异:

  1. das_hello_exec.s的开头增加了有关于_init函数的实现。

  2. das_hello_exec.s中新增了一些函数,尤其是标准库函数printf()、atoi()、getchar()、puts()与系统调用函数exit()、sleep()的语言实现。

  3. das_hello_exec.s的main函数前后新增了有关于_start和_fini函数的实现。根据名称盘以及行为判断,其应该分别会在程序开始和结束的时候调用。


    图26 : 通过diff工具查看两个文件的内容差异。绿色部分是das_hello_exec.s相较于das_hello.s多出来的部分

     

  4. das_hello_exec.s中不再出现形同“R_X86_64_PC32 .rodata-0x4”这样的语句。同时call的操作数变为虚拟内存地址,比如“call 401030”。

重定位操作是链接器通过把每个符号定义与一个内存地址关联起来,修改所有对这些符号的引用进行的。前述call指令的操作数变化,以及新函数实现的出现已经体现出重定位操作的成功进行。

5.6 hello的执行流程


图27 : 通过gdb调试来呈现和探索hello的执行流程

们通过gdb的调试来呈现和探索hello的执行流程。我们在每一个函数的入口处都打上了断点。这使得我们可以最为准确地跟踪hello的运行情况。

我们注意到,程序首先进入到_start函数。_start函数由crt0.o函数提供,它包含C运行时环境的启动代码,并且填充argv。

然后是_init函数的工作。该函数对动态链接与动态库做一些必要的工作。

然后进入到main()函数中。这里开始是程序员定义的代码逻辑。

在main()函数中,我们调用了标准库函数printf()、atoi()、getchar()、puts()与系统调用函数exit()、sleep()。其中,puts()是由以换行符结尾的printf()函数优化而来的。程序依照我们写定的逻辑调用这些函数。直到main()函数退出或者我们直接执行exit()函数。

最后是_fini函数的调用。该函数完成的工作与_init相反,用于释放对动态库的引用。

5.7 Hello的动态链接分析

我们观察可执行目标文件hello_d动态链接前后.got段的内存变化。该可执行目标文件是我们通过参数gcc -m64 -no-pie -fno-PIC编译得到的,具有动态链接性质。

态链接前后.got段的内存数据发生了变化。动态连接后,.got段装入了一些数据。


图28 : 动态链接前后.got段的内存数据变化

这是由于在动态链接之前,我们只完成了静态链接。生成的可执行目标文件是部分链接的。此时共享库中的代码和数据没有被合并到hello中。运行_init后,动态链接器加载共享库,使得内存中的程序完全链接。

5.8 本章小结

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,也是将一个或者多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件的过程。在Linux下,我们通过ld进行链接,并由此分析了hello的格式,比对了hello与hello.o的区别。

我们注意到hello的虚拟地址空间是从0x00400000开始的,我们同时观察了一部分的虚拟地址空间,比对它们与二进制文件内的数据。

我们同时分析了链接的重定位过程、hello的执行流程和hello的动态链接过程。

第6章 hello进程管理

6.1 进程的概念与作用

在面向进程设计的操作系统中,进程是程序的基本执行实体。而在面向线程设计的操作系统中,进程是线程的容器,其本身不是操作系统的基本执行单位。[11]

进程是操作系统内核对一个正在运行的程序的抽象,是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。[9]

用户下达执行程序的命令以后就会产生进程。同一程序可以产生多个进程。进程需要一些资源进行工作——CPU使用实验、存储器、文件和IO设备。

进程的作用在于提供给了应用程序两个抽象:一个独立的逻辑控制流,和一个私有的地址空间。

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

Shell是用户与操作系统交互的接口,作为一个用户对操作系统命令的解释器存在。它解析用户的命令,并将相应的信息传入内核。用户通过Shell与操作系统内核进行交互。

Bourne shell简称sh,是Version 7 Unix的默认Unix shell。其由AT&T编写,属于系统管理shell,原作者是Stephen R. Bourne。[12]

Bash是Bourne shell的后继兼容版本与开放源代码版本,它的名称来自Bourne shell(sh)的一个双关语(Bourne again/born again):Bourne-Again SHell。[13]

壳Shell-bash的处理流程如下:

  1. Shell读取用户的输入;

  2. Shell判断用户的输入是否为内置的命令,如是,则执行,如不是,转下一步;

  3. Shell判断用户的输入是否为一个可执行文件,如是,则转下一步,如不是,则根据语义、语法上的错误进行报错;

  4. Shell根据用户的输入构建参数和环境变量,通过fork()创建子进程,通过execve()函数加载可执行文件;

  5. Shell等待子进程运行结束,回收之;循环回第一步。

6.3 Hello的fork进程创建过程

以在Shell中运行hello为例:

sh-5.2$ ./hello

用法: Hello 学号 姓名 手机号 秒数!

./hello不是Shell的一个内置命令,所以Shell会认为它是一个可执行的目标文件,通过加载器调用它。

当Shell运行一个程序的时候,作为父进程的Shell通过fork()生成子进程,这个子进程与父进程使用相同但是独立的地址空间,并且和父进程共享文件。但是父进程与子进程的PID不同。

子进程一般与父进程属于同一进程组。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件。其函数原型如下。

int execve (const char *filename, char *const argv [], char *const envp[]);

其带有参数列表argv和环境变量列表envp。

它的工作包含如下的这些:

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

  2. 映射私有区:为hello的.text、.data、.rodata、.bss等区域创建新的区域结构,这些结构都是私有的,并且写时复制。

  3. 映射共享区:为hello映射一些与其他应用程序共享的数据,比如共享库libc.so。

  4. 设置PC:设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行

进程的上下文信息,指的是程序正确运行所需的状态。它一般回包含一些寄存器的值,同时带有用户栈、状态寄存器、内核栈等数据对象。内核调度新的进程运行以后,它会进行上下文切换,上下文切换是保存以前进程的上下文,恢复新恢复进程被保存的上下文。

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

我们知道计算机运行程序的本质其实是CPU不断从PC指向的地址中取出指令并执行。这个操作在时间上相串联叫逻辑控制流。操作系统对进程的运行进行调度,内核可以决定在某些时刻抢占当前进程,并重新开始一个之前被抢占的进程。内核通过来回的抢占,使得进程之间完成了CPU时间以及其他计算资源的分配,这个过程成为调度。

内核通过上下文切换机制来进行这种调度。上下文切换是在内核中一个名为调度器的部分中完成的。

用户模式和内核模式是由此划分的。用户模式和内核模式通过某个处理器中的控制寄存器来使得该寄存器描述当前进程享有的特权。当设置了模式位以后,我们就认为进程运行在内核模式中,否则则运行在用户模式中。运行在内核模式中的代码可以执行指令集中的任何指令,并且可以访问内存中的任何位置。[9]

内核通过保存进程加载的寄存器和切换虚拟地址空间来进行进程的调度。

当我们运行hello时,hello运行到sleep函数之前,是在用户模式。调用sleep时,进入到内核模式,内核处理休眠请求,并开始计时。计时完毕以后,内核发出计时完毕,恢复hello运行的信号,然后返回用户模式。这其中内核模式与用户模式的切换都发生了上下文切换。

6.6 hello的异常与信号处理

以下格式自行编排,编辑时删除

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

序执行过程中我们进行乱按,包括输入回车。程序没有对我们的输入作出反应。而我们的输入滞留在缓冲区中,被shell所反应。


图29 : 程序运行时,键盘胡乱输入

程序运行过程中我们输入Ctrl+Z,根据shell的输出,我们判断程序被挂起。此时我们可以通过内置命令fg来将我们刚才挂起的hello进程转为前台任务。


图30 : 在程序运行过程中输入Ctrl+Z

转为前台任务后,hello输出剩下的八行输出。


图31 : 在Ctrl+Z以后穿插运行ps、jobs等命令


 

通过在Ctrl+Z以后的程序穿插运行ps、jobs等命令,我们注意到,通过Ctrl+Z挂起的程序会进入到后台,可以通过jobs和ps观察到其具体状态以及PID号。我们通过kill向程序发送终止信号,程序并不会马上处理,而是等我们使用fg将进程转化为前台作业以后处理。

程序运行的过程输入Ctrl+C,程序会被立刻终止。通过ps工具我们可以发现相应的进程已经被系统所回收。


图32 : 在程序运行的过程中输入Ctrl+C

6.7本章小结

进程是操作系统内核对一个正在运行的程序的抽象,是一个执行中程序的实例。用户通过Shell与操作系统内核进行交互。Shell执行特定的流程,对命令进行解释。

由于hello对于Shell来说是外部的程序,所以Shell会通过fork()创建一个子进程,并通过execve()去执行它。

我们由此进一步讨论了进程的调度问题,讨论了内核对程序运行所需状态的抽象——上下文,以及它的切换——上下文切换。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个闲钱被抢占了的线程。这种决策成为调度。调度通过上下文切换的机制进行。同时我们还讨论了用户模式和内核模式的存在。

我们最后讨论了程序的异常控制流,讨论了不同情况下hello对异常和信号的处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

为了提高程序的管理效率,减少失误,现代操作系统提供了一种对内存的抽象称为虚拟内存。以下介绍虚拟内存相关的概念:

  1. 逻辑地址:由程序产生的与段相关的偏移地址部分。比如hello.o中call指令通过逻辑地址实现函数跳转。

  2. 线性地址:逻辑地址变换为物理地址的中间结果。hello的代码会产生段中的偏移地址,加上相对应的基址以后形成一个线性地址。

  3. 虚拟地址:在保护模式下,hello运行在虚拟地址空间中,其使用逻辑地址访问存储器。

  4. 物理地址:内存单元的真正地址。CPU通过物理地址访问物理内存。

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

段式管理就是将逻辑地址直接转换成物理地址,CPU不支持分页机制。在保护模式中,使用段描述符作为下标,到全局描述符表(GDT)或者局部描述符表(LDT)中查表获得段地址。段地址与偏移地址相加,得到线性地址。

段信息无法直接存放在段寄存器中。Intel的解决方案将段描述符集中存放在GDT或LDT中,段寄存器中存放段描述符在GDT或LDT内的。段描述符的T1字段告诉我们当前要转换的是GDT中的段还是LDT中的段。转换得到一个数组的地址和大小。取段描述符的前13位,可以在这个结果数组中查找基地址。将基地址加上偏移地址,则得到结果。

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

线性地址的组成包括虚拟页号和虚拟页偏移。前半部分是虚拟页号,后半部分是虚拟页偏移。

CPU将逻辑地址转换为虚拟地址,虚拟地址在访问主存前转换为物理地址。CPU上的内存管理单元(MMU)会利用主存中的查询表翻译虚拟地址为物理地址,CPU会根据翻译结果访问物理内存。

页表是将虚拟页映射到物理页的数据结构,由页表条目PTE组成。页表条目由一个有效位和一个字段组成。有效位为1的时候,页表条目合法,其后字段指向一个物理内存位置或者一个磁盘位置;否则,我们用一个空地址表示这个虚拟页还未分配。

MMU使用虚拟页号VPN来在虚拟页表中查找并选择对应的PTE,得到对应的物理页号PPN,PTE中的物理页号PPN和物理页偏移量PPO组成物理地址。其中物理页偏移量PPO的值等于虚拟页偏移量VPO的值。

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

TLB称为翻译后备缓冲器。它作为虚拟寻址的缓存参与到寻址的过程中。每次CPU产生一个虚拟地址,MMU就从TLB中取出对应的PTE。如果这一步成功,那么MMU得到一个物理地址,并将其发送给cache,或者籍由总线发送到主存。最后cache或者主存将数据发回给CPU;否则,MMU尝试从L1缓存中取出对应的PTE,然后将对应的PTE放入到TLB中(如若不成则继续向更低层级的存储器请求PTE,逐级向下)。

多级页表则意味着我们的PTE中存储的地址可能并不指向一个虚拟页存页,也可能指向下一级页表。

四级页表下VA向PA的变换过程如下:

  1. 处理器生成一个虚拟地址。

  2. MMU从TLB中取出对应的PTE。

  3. MMU将这个虚拟地址翻译为一个物理地址,并且将它发送给高速缓存/主存。

  4. 高速缓存/主存将请求的数据字返回给CPU。CPU判断请求回来的数据字内容是PTE还是最终请求的数据字。如是前者,则继续上述步骤。

如若对TLB请求内容失败,则向L1缓存请求数据,逐级向下。

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

当CPU得到物理地址PA以后,物理地址分为组标记、组索引和块偏移三部分。

首先根据组索引选择L1中的组,然后查找组标记与PA一致并且有效位有效的行,最后根据块偏移在组中取数据。

如若找不到有效的行,则向下一级高速缓存请求数据,步骤同上。

7.6 hello进程fork时的内存映射

当前进程通过fork函数进行系统调用,与内核交互,内核创建一个新进程,并创建该进程所需的各种数据结构,进行PID的分配。

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

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。 [9]

7.7 hello进程execve时的内存映射

execve在加载并执行hello的时候需要如下的步骤:

  1. 删除已经存在的用户区域:删除当前进程虚拟地址用户部分中已经存在的区域结构。

  2. 映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有写时复制的。

  3. 映射共享区域:如果hello与共享对象链接,则我们动态链接这些共享对象,在映射到用户虚拟地址空间的共享区域内。

  4. 设置程序计数器:execve设置当前进程上下文中的程序计数器。[9]

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

缺页指的是当CPU产生的一个虚拟地址并不在DRAM缓存中的情况,即DRAM缓存不命中。

在虚拟内存的习惯说法中,块被称为页。在磁盘和内存之间传送页成为交换或者页面调度。[9]

缺页引发缺页异常,进而调用内核中的缺页异常处理程序,缺页异常处理程序会根据其调度策略选择一个牺牲页,用要读取的地址的内容替换它,然后返回。内核重新启动导致缺页的指令。该指令会把导致缺页的虚拟地址重新发送到地址翻译硬件。

如果牺牲页是一个脏页,则它被替换之前会被先写回磁盘。

7.9动态存储分配管理

动态内存管理由动态内存分配器进行。动态内存分配器维护一个进程的虚拟内存区域,称为堆。[9]

分配器将堆视为一组大小不同的块的集合来维护,每个块就是一个连续的虚拟内存片,或已分配,或空闲。分配器可以分为显式分配器和隐式分配器,显式分配器要求应用程序显式地释放任何已分配的块,而隐式分配器要求分配器检测一个块何时不再被程序使用并且释放这些不再被程序使用的快。

我们使用C语言编写hello,因而hello使用显式分配器。

hello中的printf()会调用malloc(),malloc()是用于向堆申请块的函数。

malloc()函数的原型如下:

void *malloc(size_t size);

malloc()函数返回一个指针,该指针指向大小至少为size的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。如果程序通过malloc()申请的内存比堆中的可用内存还要大,则malloc()返回NULL。

使用malloc()申请的内存只能使用free()函数进行释放。

显式分配器需要完成的工作包含:

  1. 处理任意请求序列;

  2. 立即响应请求;

  3. 只使用堆;

  4. 对齐申请得到的数据块;

  5. 不修改已分配的块。

7.10本章小结

为了提高程序的管理效率,减少失误,现代操作系统提供了一种对内存的抽象称为虚拟内存。它有至少两种管理方式;其中段式管理就是将逻辑地址直接转换成物理地址;页式管理则将线性地址的拆分为两部分——前半部分是虚拟页号,后半部分是虚拟页偏移——同时将内存视作一个个内存页,以通过页号和页偏移唯一地定位。

TLB称为翻译后备缓冲器。它是CPU翻译地址的时候所使用的缓存。其运作的逻辑与高速缓存Cache很像。在三级Cache支持下进行物理内存访问,我们会将物理地址拆分成组标记、组索引、块偏移三部分,然后通过组索引确定组,通过组标记确定块,通过块中有效位确定块的有效性,然后通过块偏移取数据。

我们还讨论了使用fork()、execve()这两个系统调用时候的内存映射情况以及缺页——DRAM缓存不命中,及其造成的缺页中断处理,以及动态内存管理的情况。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux的设计思想之一,是“一切皆文件”。在Linux中,所有的I/O接口都被模型化为文件,所有的输入和输出都被当作对应文件的读写来进行。这使得操作系统为所有的设备(包括I/O设备)抽象出一个统一的接口。

程序通过对对应I/O设备的文件进行读写操作进行对I/O设备的管理。

应用与内核通讯,请求打开文件,而内核返回一个非负整数。该非负整数称为文件描述符,用以表示对应的设备以及该设备对应的文件。内核记录所有的文件描述符,应用程序在使用系统调用与内核通讯的时候只需要提供这一描述符,就可以访问到同一I/O设备。

这种抽象使得应用程序能够统一且一致地进行I/O操作。

8.2 简述Unix IO接口及其函数

Unix I/O接口及其函数主要包含如下几个函数:

int open(char* filename, int flags, mode_t mode);

int close(int fd);

ssize_t read(int fd, void *buf, size_t n);

ssize_t write(int fd, const void *buf, size_t n);

open函数将filename字符串传入的路径转换为一个文件描述符,并且返回这个描述符。返回的描述符总是当前进程中没有打开的最小描述符。flags指明了进程访问该文件的方式,mode指明了进程访问该文件的权限。

close函数将filename字符串传入的文件描述符关闭。内核会释放与该文件描述符相关的资源。再次使用同一I/O设备/文件的时候需要重新通过open函数申请文件描述符。

read函数从传入的文件描述符中读取数据到缓冲区*buf,读入的数据最多为n字节。返回值ssize_t如为-1,则说明遇到错误,如为0,则说明读到文件尾,否则返回值表示实际传诵的字节数量。

write函数写入数据到传入的文件描述符中,数据的来源是缓冲区*buf,写入的数据最多为n字节。

8.3 printf的实现分析

int printf(const char *fmt, ...) {

int i;

char buf[256];

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

i = vsprintf(buf, fmt, arg);

write(buf, i);

return i;

}

printf()函数的实现如上。

printf接受一个名为*fmt的格式串,将匹配到的参数按照fmt的格式进行输出。其使用的两个外部参数——vsprintf()和write()。

vsprintf()返回所打印字符串的长度,其作用是格式化,产生格式化输出。

随即write()函数将buf中的i个字符(i是vsprintf()的返回值)复制到显示器上。write函数执行的时候陷阱-系统调用 int 0x80或syscall将字符串的ASCII码从寄存器复制到显卡的显存中,并由字符显示驱动子程序进行从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)的转换。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

现代的终端应用可能会将可输出字符的范围扩展到系统字符集的范围,并且不一定是直接由字符显示驱动子程序进行输出,而是可能由统一的图形接口进行输出。

8.4 getchar的实现分析

int getchar(void)

{

static char buf[BUFSIZ];

static char *bb = buf;

static int n = 0;

if (n == 0)

{

n = read(0, buf, BUFSIZ);

bb = buf;

}

return (--n >= 0) ? (unsigned char)*bb++ : EOF;

}

getchar()函数的实现如上。

getchar()从标准输入中读入一个字符,并将该字符强制转换成int形式返回。如果到达文件尾EOF或发生读错误,则返回EOF。

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

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

8.5本章小结

Linux的设计思想之一,是“一切皆文件”。在Linux中,所有的I/O接口都被模型化为文件,所有的输入和输出都被当作对应文件的读写来进行。

我们展示了Linux基本的几个I/O函数,包括open()、close()、read()和write()。同时对printf()和getchar()的实现作了分析。

结论

  1. 首先我们对hello.c文件进行预处理,得到hello.i。它是hello.c预处理后的文件。

  2. 然后我们对hello.i进行编译处理,得到hello.s。它是hello.i的编译结果,其内部为编译语言。

  3. 我们对hello.s进行汇编,得到hello.o。这一文件称为可重定位目标文件,其内是机器语言。

  4. 我们对hello.o,以及系统提供的其他库文件进行链接,得到最后的可执行目标程序hello。

  5. 我们在shell中输入./hello <参数>运行这一程序。

  6. shell分析输入内容,判定./hello不是一个内置指令,于是fork()产生子进程,并通过execve()执行当前目录下的hello程序。

  7. 当前目录下的hello程序装入内存。系统为其分配PID,分配虚拟内存。

  8. 程序在_init函数中动态链接共享库,为动态链接的数据对象重定位。

  9. 程序从给入的参数读取信息,进行逻辑判断。

  10. 程序进行I/O输出。C标准库实现的I/O输出其实现调用了Unix I/O的系统接口。我们在屏幕上得到我们想要的结果。

  11. 程序运行的过程中遇到超出设计的情况,比如超出预料的输出,以及Ctrl+C、Ctrl+Z这样的键盘信号,可以用fg、bg决定程序是在shell的前台运行还是在后台运行,并且可以通过kill发送杀死程序的信号(SIGLKILL)。

操作系统调度计算机资源。在计算机软硬件的通力配合之下,hello程序从诞生到结束,完成了它的使命。

通过学习《计算机系统》,我对计算机这一整体有了更多的了解:计算机系统由硬件和系统软件组成,这两部分共同工作来运行应用程序。我学习到了计算机如何表示程序和信息,编写代码的时候如何优化程序性能,计算机如何设计和使用存储器,链接的概念和作用,异常控制流如何影响我们的程序,虚拟内存这一概念如何被抽象出来等知识内容,加深了对计算机的了解。

计算机系统编译并执行程序的过程严丝合缝,上一过程的输出往往是下一过程的输入。我们从源代码到程序,从程序到进程,从由程序语言写就的片段到计算机系统实际的反应,我们在这一过程中实现了对计算机的控制。

一个复杂的系统往往需要多方面的配合协作才能更好地实现功能,而抽象是设计这一系统,简化这种配合协作的利器。虚拟内存抽象了对内存的映射,Unix I/O抽象了对I/O设备的映射。我们通过这种统筹兼顾的方式,杂取百家之长,完成了对计算机系统的应用工作。

应用软件使用贴近计算机系统的设计,进而实现开发的高效。一些算法,比如B+树和动态规划,以及一些应用软件的设计,比如网站和数据库,其本质和计算机系统是一样的。nginx、LLVM和JVM的内部的设计和计算机系统的设计也相类。这很好地说明了计算机系统作为一个思想工具的有效性。

计算机系统作为计算机学科中最基本的概念和思考工具,为我们未来的职业生涯添加了许多活力。

附件

本论文使用的源代码文件为hello.c,目标二进制文件为hello。

在编译的过程中生成的中间结果文件如下:

  • hello.i:修改了的源程序。是预处理器对源代码进行预处理以后得到的文本文件,其中已经完成了对头文件的包含、宏扩展等工作。

  • hello.s:汇编程序。是编译器根据hello.i生成的汇编代码文本。需要经过汇编器生成二进制程序以后方可执行。该程序包含函数main()的定义。

  • hello.o:可重定位目标程序。由汇编器翻译得到,是二进制程序,其内是机器语言指令。经过链接可以得到目标的二进制文件。

  • hello:汇编并链接得到的可执行程序文件。该程序可以直接执行,是hello.c的直接编译结果。

  • das_hello.s:通过objdump对hello.o进行反汇编的结果。

  • das_hello_exec.s:通过objdump对hello进行反汇编的结果。

  • hello_d:动态链接的hello。

参考文献

[1] BRIAN W. KERNIGHAN, DENNIS M. RITCHIE著 ; 徐宝文, 李志译., KERNIGHAN B W, RITCHIE D M, 等. C程序设计语言[M]. 第2版. Beijing: 机械工业出版社, 2004.

[2] 孤儿进程与僵尸进程[总结] - Rabbit_Dale - 博客园[EB/OL]. [2024-05-22]. https://www.cnblogs.com/Anker/p/3271773.html.

[3] C预处理器[Z/OL]//维基百科,自由的百科全书. (2023-09-18)[2024-05-22]. https://zh.wikipedia.org/w/index.php?title=C%E9%A2%84%E5%A4%84%E7%90%86%E5%99%A8&oldid=78985110.

[4] ISO/IEC. ISO/IEC 9899:2018[Z]. 2018.

[5] NORUM C. Answer to “What does ‘# 1 “/usr/include/stdio.h” 1 3 4’ mean in ‘gcc -E’ output?”[EB/OL]//Stack Overflow. (2013-05-31)[2024-05-23]. https://stackoverflow.com/a/16850128.

[6] Line Control (The C Preprocessor)[EB/OL]. [2024-05-23]. https://gcc.gnu.org/onlinedocs/cpp/Line-Control.html.

[7] Preprocessor Output (The C Preprocessor)[EB/OL]. [2024-05-23]. https://gcc.gnu.org/onlinedocs/cpp/Preprocessor-Output.html.

[8] STEPHEN PRATA. C primer plus[M]. 5th ed. Indianapolis, Ind: Sams, 2005.

[9] [美] 兰德尔 E. 布莱恩特(RANDAL E. BRYANT), 大卫 R. 奥哈拉伦(DAVID R. O’HALLARON)著 ; 龚奕利, 贺莲译., BRYANT R E, O’HALLARON D R. 深入理解计算机系统[M]. 龚奕利, 贺莲, 译. 原书第3版. Beijing: 机械工业出版社, 2016.

[10] 链接器[Z/OL]//维基百科,自由的百科全书. (2021-06-22)[2024-05-25]. https://zh.wikipedia.org/w/index.php?title=%E9%93%BE%E6%8E%A5%E5%99%A8&oldid=66209024.

[11] 行程[Z/OL]//维基百科,自由的百科全书. (2022-12-02)[2024-05-26]. https://zh.wikipedia.org/w/index.php?title=%E8%A1%8C%E7%A8%8B&oldid=74878169.

[12] Bourne shell[Z/OL]//维基百科,自由的百科全书. (2023-09-11)[2024-05-26]. https://zh.wikipedia.org/w/index.php?title=Bourne_shell&oldid=78881246.

[13] Bash[Z/OL]//维基百科,自由的百科全书. (2023-06-25)[2024-05-26]. https://zh.wikipedia.org/w/index.php?title=Bash&oldid=77816833.

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值