C/C++编译与链接 程序员的自我修养:链接 装载和库

程序员的自我修养:链接 装载和库

一. 操作系统基本知识

1.计算机多如牛毛的硬件设备中,有三个部件最为关键,它们分别是中央处理器CPU、内存和I/O控制芯片;对于普通应用程序开发者来说,他们似乎除了要关心CPU以外,其他的硬件细节基本不用关心,对于一些高级平台的开发者来说(如Java、.NET或脚本语言开发者),连CPU都不需要关心,因为这些平台为它们提供了一个通用的抽象的计算机,他们只要关心这个抽象的计算机就可以了。

2.由于CPU核心频率的提升,导致内存跟不上CPU的速度,于是产生了与内存频率一致的系统总线,而CPU采用倍频的方式与系统总线进行通信。接着随着图形化的操作系统普及,特别是3D游戏和多媒体的发展,使得图形芯片需要跟CPU和内存之间大量交换数据,慢速的I/O总线已经无法满足图形设备的巨大需求。为了协调CPU、内存和高速的图形设备,人们专门设计了一个高速的北桥芯片,以便它们之间能够高速地交换数据。

3.“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
4.多任务(Multi-tasking)系统,操作系统接管了所有的硬件资源,并且本身运行在一个受硬件保护的级别。所有的应用程序都以进程(Process)的方式运行在比操作系统权限更低的级别,每个进程都有自己独立的地址空间,使得进程之间的地址空间相互隔离。CPU由操作系统统一进行分配,每个进程根据进程优先级的高低都有机会得到CPU,但是,如果运行时间超出了一定的时间,操作系统会暂停该进程,将CPU资源分配给其他等待运行的进程。这种CPU的分配方式即所谓的抢占式(Preemptive),操作系统可以强制剥夺CPU资源并且分配给它认为目前最需要的进程。

4.我们把程序给出的地址看作是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。这样,只要我们能够妥善地控制这个虚拟地址到物理地址的映射过程,就可以保证任意一个程序所能够访问的物理内存区域跟另外一个程序相互不重叠,以达到地址空间隔离的效果。

5.所谓的地址空间是个比较抽象的概念,你可以把它想象成一个很大的数组,每个数组的元素是一个字节,而这个数组大小由地址空间的地址长度决定,比如32位的地址空间的大小为 2^32 = 4 294 967 296 字节,即4GB,地址空间有效的地址是 0~4 294 967 295,用十六进制表示就是0x00000000~0xFFFFFFFF。地址空间分两种:虚拟地址空间(Virtual Address Space)和物理地址空间(Physical Address Space)。

5.是分段的这种方法还是没有解决我们的第二个问题,即内存使用效率的问题。分段对内存区域的映射还是按照程序为单位,如果内存不足,被换入换出到磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。事实上,根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都是不会被用到的。人们很自然地想到了更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。这种方法就是分页

6.当线程数量小于等于处理器数量时(并且操作系统支持多处理器),线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此之间互不相干。但对于线程数量大于处理器数量的情况,线程的并发会受到一些阻碍,因为此时至少有一个处理器会运行多个线程。

7.线程调度自多任务操作系统问世以来就不断地被提出不同的方案和算法。现在主流的调度方式尽管各不相同,但都带有优先级调度(Priority Schedule)和轮转法(Round Robin)的痕迹。所谓轮转法,即是之前提到的让各个线程轮流执行一小段时间的方法。这决定了线程之间交错执行的特点。而优先级调度则决定了线程按照什么顺序轮流执行。

8.对于允许多个线程并发访问的资源,多元信号量简称信号量(Semaphore)

9.根据操作系统内核是否对线程可感知,可以把线程分为内核线程用户线程

二. 程序执行过程

1.

是预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking)

预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。
 预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。

2

先源代码程序被输入到扫描器(Scanner),扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于有限状态机(Finite State Machine)的算法可以很轻松地将源代码的字符序列分割成一系列的记号

3

语法分析器(Grammar Parser)将对由扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree)。整个分析过程采用了上下文无关语法(Context-free Grammar)的分析手段,

4

语义分析,由语义分析器(Semantic Analyzer)来完成。语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。比如C语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的;比如同样一个指针和一个浮点数做乘法运算是否合法等。编译器所能分析的语义是静态语义(Static Semantic),所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义(Dynamic Semantic)就是只有在运行期才能确定的语义。

5

源码级优化器(Source Code Optimizer)在不同编译器中可能会有不同的定义或有一些其他的差异。源代码级优化器会在源代码级别进行优化

6

代码生成器(Code Generator)和目标代码优化器(Target Code Optimizer)。让我们先来看看代码生成器。代码生成器将中间代码转换成目标机器代码。目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等

7

程序并不是一写好就永远不变化的,它可能会经常被修改。比如我们在第1条指令之后、第5条指令之前插入了一条或多条指令,那么第5条指令及后面的指令的位置将会相应地往后移动,原先第一条指令的低4位的数字将需要相应地调整。在这个过程中,程序员需要人工重新计算每个子程序或跳转的目标地址。当程序修改的时候,这些位置都要重新计算,十分繁琐又耗时,并且很容易出错。这种重新计算各个目标的地址过程被叫做重定位(Relocation)。
最常见的属于静态语言的C/C++模块之间通信有两种方式,一种是模块间的函数调用,另外一种是模块间的变量访问。
人们把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接(Linking)。链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等这些步骤。

8

程序源代码编译后的机器指令经常被放在代码段(Code Section)里,代码段常见的名字有“.code”或“.text”;全局变量和局部静态变量数据经常放在数据段(Data Section),数据段的一般名字都叫“.data”。
程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。
(1)是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。
(2) 另外一方面是对于现代的CPU来说,它们有着极为强大的缓存(Cache)体系。。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
(3)就是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须要保存一份该程序的指令部分。

三.程序的编译

3.1程序指令数据的存储

**程序员的自我修养: 1 2 3 4 6 10章

int gdata1 = 10;//全局变量             这一块是数据
int gdata2 = 0;
int gdata3;//未初始化的全局变量,gdata3是弱符号。经过编译得到.obj文件,不能说在程序里面用的gdata3就是这个。因为编译完成后要链接,链接的时候所有obj文件一起链接,若在其他的obj文件会找到gdata3的强符号或者找到它的内存占有量更大的弱符号,所以不一定选用它。它在COM块(不在任何段):未决定的符号。链接的时候才等待选择。

static int gdata4 = 11;//静态全局变量    本地的符号,在链接的时候根本不看          这一块是数据
static int gdata5 = 0;
static int gdata6;//它存在于.bss段是因为:静态变量 只是本文件可见
//所以可见 链接器只对.obj文件所有的global符号进行处理,对local符号不做任何处理。
/*
#include<stdio.h>    里面只有puts()的声明,定义在库里面。
在编译c的时候,自动链接一个libc.so:C语言库的动态链接库(共享库)(共享库内存在栈和堆的中间)。

int main(int argc,char**argv,char**environ)main函数原型
{
	argc:命令行参数的个数  不传的时候默认带的默认命令行参数是可执行文件的绝对路径
	argv:命令行参数
	environ:环境变量 传了库,头文件程序编译链接所依赖的路径
	(char**argv,char**environ 都是在栈的高内存上存的。)
}

*/
int main()
{
	int a = 12, b = 0, c;//局部变量  这一块是指令,没符号不是数据。a b c在栈上



	static int d = 13;//静态局部变量                        这一块是数据
	static int e = 0;
	static int f;

	//char*p="hello world";//常量字符串 存在 只读数据段

	return 0;                                        //这一块是指令
}

程序运行会被分配虚拟空间,虚拟空间大小与CPU大小有关,CPU寻址空间即为分配的虚拟空间大小。虚拟空间中又分为用户空间和内核空间。其中用户空间中指令和数据分别存储。红色部分,包括局部变量,其他指令等存储在指令空间.text中。黑色部分,全局变量等存储在数据空间.data。未初始化变量放在.bss中。此时程序尚且不能运行。程序的运行须在用户空间多一个.stack 栈空间。

在这里插入图片描述

1 变量的生存周期以及作用域(C语言中)。

2 C和C++代码->编译链接->可执行程序(可执行文件本身存放在磁盘)。运行时,可执行程序必须加载到内存中运行。因为CPU从内存里访问数据效率要比在磁盘里访问效率高太多。
3 那么要把什么东西加载到内存里呢?(任何语言写代码只产生了两种东西:指令 数据)

操作系统为了屏蔽底层的硬件的差异,为应用层的用户编写程序时调用统一的接口。
为了屏蔽I/O的差异:基于I/O层,操作系统提供了一个虚拟文件系统VFS。
为了屏蔽I/O和内存的差异:(也是作为一个资源分配管理的单位)虚拟存储器即虚拟内存。
为了屏蔽I/O、CPU、内存的差异:(也是作为这三个部件资源调度的单位)进程。
在这里插入图片描述

4 所以说操作系统内核屏蔽底层硬件,我们只要有操作系统,代码生成的可执行文件是不可能直接加载到物理内存上。而是加载到虚拟内存上, 它是多大与系统位数有关。 跟CPU位数有关:*一次能加以运算的最长的整数的宽度。* 即ALU的宽度 (也即数据总线的条数)位数高 能力强。 CPU执行运算的,在ALU里运算数据。默认32位 X86的Linux内核。即CPU位数32位。
5 数据是哪里来的?从数据总线来的。CPU的位数可不是地址总线的条数。
暂且来说 程序运行在虚拟内存上。虚拟内存实际上不存在,是逻辑上抽象出来的。(虚拟内存是物理内存的管理办法)
代码->编译链接->可执行程序。在可执行程序运行时,操作系统都会给它提供一个虚拟内存;32位那么它的大小2的32次方。也即CPU寻址的能力即4G。程序只要一运行,没有直接跑到物理内存上(OS屏蔽底层的细节差异,由OS统一管理底层资源的分配),只能从内核那得到一块虚拟地址空间(4G 2的32次方)。
6 虚拟地址空间是怎么布局的:
0x00000000低地址-----0xffffffff高地址。分成两部分:3G(用户空间 给用户态应用程序运行) 1G(内核空间 给操作系统内核代码运行)。系统可以运行很多进程:他们的用户空间都是独立的;内核空间是共享的。
最上面128M(0x00000000-----0x08048000)是禁止访问的。接下来是代码段(.text 放指令,这段只能读且可执行) 数据段 可读可写(.data) (.bss)数据放((.data) 和(.bss))。指令加载后放在代码段。数据放在数据段(初始化了的且初始值是不为0的值放在.data)(没有初始化了的或者是初始化为0的值放在.bss 更好的节省空间)这里的空间:(虚拟地址空间)。局部变量属于 指令
.bss段下面放的是给堆heap分配的内存。如果当前程序用到了库里面的函数,那么在堆和栈中间放共享库(puts gets scanf printf等函数的定义的地方)。在共享库下面放的是栈。程序运行进入函数 函数的运行必须要提供栈内存,局部变量a b c 都在栈上。在栈的下面是命令行参数和环境变量。

代码段(.text 指令) 数据段(.data) (.bss)这三段是固定不变的。 只有涉及到malloc和new时才有堆。

#include<stdio.h>    里面只有puts()的声明,没有定义,但定义在库里面。
在编译c的时候,自动链接一个libc.so: **C语言库的动态链接库(共享库)** (共享库的内存在栈和堆的中间)。

int main(int argc,char**argv,char**environ)main函数原型
{
	argc:命令行参数的个数  不传的时候,默认带的默认命令行参数是可执行文件的绝对路径
	argv:命令行参数
	environ:环境变量:传了库,头文件程序编译链接所依赖的路径
	(char**argv,char**environ 都是在栈的高内存上存的。)
}

内核空间分为3部分:
16M的(ZONE_DMA:直接内存访问。加快磁盘和内存交换数据)
:在没有DMA技术之前,我们的磁盘和内存之间交互数据,数据必须得通过总线流经CPU的寄存器才能达到对方。但是这是对CPU的一个极大浪费!!!现在有了DMA,比如在磁盘加载一个文件到内存当中,数据从磁盘经过系统总线流向物理内存当中时候,不用经过CPU的寄存器了。比如:此时的进程在打开一个文件,涉及到磁盘上数据流向物理内存中,这时的CPU是空闲的 可以调度其他的进程。

892M的(ZONE_NORMAL:) 内核最重要的部分。

128M的(ZONE_HIGHMEM:高端内存。在32位 X86的Linux系统,在我们内核映射高于1G的物理内存的时候会用到。而64位的用不上,因为其内核空间达到了512G)

3.2编译阶段:

1 预编译阶段->main.i;
(做了什么:删除注释;处理以#开头的预编译指令。不作任何有效的类型信息的检查)
2 编译阶段->main.s(汇编文件);语法 语义 词法的分析;编译代码;优化代码;汇总所有的符号;
3 汇编阶段->.o或者.obj的二进制可重定位目标文件;根据特定平台,把汇编指令转化成特定平台的机器码;构建目标文件格式(.o / .obj)

obj文件(其组成格式是:。以及不能运行的原因是:没有重定位)在二进制可重定位中非常重要的是 符号表。数据是一定产生符号的,函数只产生一个符号即函数名。局部变量a b c 不产生,d e f 是数据,所以产生的。编译的过程:不给符号分配内存地址,指令在编译中涉及到符号或者涉及到数据的存取(数据符号没有分配内存地址)所以在指令上 这个符号的内存地址只能是个 0x00000000。
所以其填数据的地址都是0x00000000,填函数名的地址都是跟下一行指令地址的偏移量。

main.o 它是一个重定位的文件,不是可执行文件 Linux系统
main.o组成格式
:ELF Header文件头 包含:头大小:52字节。
这个文件头包含:OS/ABI:程序运行的平台;Machine:运行的体系;Entry point address:入口地址(此时是0地址,不能访问)。
程序代码仅在编译过程 不给符号分配内存地址。链接时候分配。
文件头ELF Header接着就是代码段(编译得到的指令)其是从0x34开始偏移的,对齐方式是4字节。不够的情况下4字节补齐。再接下来是数据段()。再下来是.bss段(不占文件的空间,它占虚拟地址空间,它省的是文件的空间)

这个文件头ELF Header里面记录了 是什么样的程序,适合用在什么样的平台和体系上。obj文件都是一块一块的,每一块都是一个段。最后一个段的偏移量+段的大小就是整个文件的大小(348+4C=394 916字节)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在这里插入图片描述

问题1 既然文件不存.bss段 那系统怎么知道这些变量的?
(读文件的文件头就可以知道:文件里 段表。段表 里就保存了这个文件里面都有哪些段 段的起始偏移量 段的大小。段表在文件中的偏移量 记录在文件头中。通过读文件头就可以知道section table在哪里了? .bss段(不需要存什么初始值)里放的数据一打印都是0,操作系统给.bss段数据默认赋值0。所以没必要用存储它,但操作系统得知道存在。只用把这些所有段的详细信息:占多大内存等 记录在段表即可)虽说是 data段信息也在段表中存储,但还得这个段还要单独存储:数据段每个数据都有自己的初始值,必须得记录。
问题2 那为什么bss段少了一个变量呢? 一个工程,多个文件在编译的时候,各自是分离编译的。在C语言里,有初始化的为强符号,没有初始化的为弱符号。如果在C语言工程里面,出现多个强符号,肯定会出错;但是一个强一个弱,最终选择强符号;如果出现多个同名的弱符号,最后会选择内存占用量最大的符号。所以说在这个编译阶段,不能保证确定我最终选用的是哪个同名符号 所以少的那个变量是gdata3 原因:没有初始化的全局变量 。这是一个弱符号,编译得到obj文件,我不能确定gdata3的地方确定是这个gdata3。在链接的时候是所有的obj文件一块链接的,我可能在其他的obj文件里找到 同名的其他的gdata3强符号或者内存占用量最大的弱符号。那么还有个问题:为什么gdata6 这个也没初始化,同样是弱符号:因为它前面的static 表示静态变量,只能本文件可见 。所以这个是local的,链接器不作处理。(在链接之后,gdata3这个弱符号在链接过程中,没有找到其他的符号,最终决定就用它了,这个时候bss就大小恢复为18 24字节,但这个bss段仍然没有占空间。
gdata1 gdata4 d 是data段
gdata2 gdata5 gdata6 e f是bss段
gdata3 在COM块:并没有在任何的段,现在是个未决定的符号。弱符号 链接阶段找

3.3链接阶段:

1 合并所有的目标文件的段,按属性合并(按照n个text段合并占一个页面 ----。)(现在采用的是: 相同属性的段进行合并,组织在一个页面上。这样合并的一个好处就是可执行文件的大小更小一些)
2 合并以后要在段表调整段偏移和段长度;
3 每个obj文件都有一个符号表,所以要合并符号表,其目的是—> 进行符号解析;分配内存地址(符号解析完成后,每个符号都得到了一个合法的虚拟地址空间的地址,即虚拟内存的地址。
合并符号表,其目的是:对所有符号的引用都找到符号的定义。
符号解析:所有obj符号表中对符号引用的地方都要找到该符号定义的地方。即确定一个符号最终只有一个定义的地方。定义的符号只有一个名字。
4 符号解析完成则符号表合并完成。然后给符号分配内存地址。再之后 符号重定位。
5 符号重定位是什么? 在代码指令的编译时在编译过程中进行的,编译过程中对符号并不分配内存地址。所以在指令中所有对于函数指令以及数据符号引用的地方填的地址都不正确。在链接过程第一步符号解析完成后,给符号分配了合理的内存地址后,所以链接器返回去要在指令text段都修改一下。 数据符号填的是绝对地址 ,但是函数符号(因涉及到指令跳转)相对于下一行地址的存的都是偏移量。 它将要跳的位置是:PC寄存器放的下一行指令的地址+偏移量。因为CPU在进行指令访问时,是从PC寄存器里取地址,当我们运行到当前指令的时候,PC寄存器放的是下一行的指令运行的地址,那在这里call 指令涉及到指令的跳转:在这里要调到其他的代码段去执行了而不是继续运行下一行指令。将要跳的位置是:PC寄存器放的下一行指令的地址+我这值给的偏移量。
链接的核心:符号的重定位。 得到可执行文件,。为什么可以执行?CPU怎么知道从哪开始执行?运行之后,指令访问数据,内存中是怎么处理的?符号的local属性和global属性决定了在链接阶段,链接器要不要处理它。链接器在链接的时候,只对所以obj文件的global符号进行处理,local的不做处理。

3.4可执行文件

可执行文件是以页面组织的,存储方式:按照段存储的。 32位系统页面大小是4K字节。段的对齐方式也是4字节对齐。段的合并是按照属性合并的。
(1)前面仍是一个ELF Header。也是一些段的组织。文件头52个字节,偏移量0x34。ELF Header下面是 program headers(可执行文件比obj文件多的部分)。 program headers里面是:一个program headers的大小是32字节;program headers有3个:两个load页,一个GUN_STACK。load的对齐方式是0x1000 即4K 一个页面的大小。第一个load页保存的是text段(可读可执行)从0x08048000开始存放,但是这一页未必就存放满了 会造成浪费但是没办法只能按页面对齐。第二个load页保存的是data bss段(可读可写)。两个load页面指示操作系统的加载器loader程序要把当前可执行程序的哪些部分加载到内存上面(放到一个页面里面)。
两个load项指示的是:按页面对齐,把可执行文件的某些段分配到一个页面上。
只有两个页的原因:程序写出来 不是指令就是数据;他们两个各用一个。按照属性相同放在一个页上。

磁盘上一个DP页:磁盘页。两个load项所记录的load页:DP1和DP2。把磁盘页往内存上加载的时候,直接加载到4G虚拟地址空间即VP。VP也是按页面为单位:很容易按照同一个单位把磁盘上的一个页面映射到虚拟地址空间的一个页面上。但是虚拟的空间还是通过操作系统把它加载到 物理内存 中即PP。PP的管理方式也是按页面为单位,在我们用某些指令或者数据要访问、执行的时候,虚拟页面也得映射到物理地址空间的一个页面上。可执行文件为什么要两个load项记录某些段在同一个页面上?因为虚拟地址空间和物理内存的管理方式都是按页面 为了方便映射。

把磁盘上的一个页面映射到虚拟地址空间的一个页面上(即怎么在虚拟地址空间上开辟内存的)的方式:mmap函数(深入理解计算机系统第9章);把虚拟地址空间上的虚拟页面映射到物理内存页面上的方式:多级页表映射。两个DP页是从可执行文件的program header那两个load项来的。

所以说obj文件不能运行的原因:没有program headers这么一段。
program headers下面是text段

(2)其入口地址Entry point address是main函数第一行指令的地址,而不是0地址。

程序的运行的过程:程序的运行就变成一个进程,分为3步:
1 创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表
2 加载代码段和数据段(其他的段都不用,也不用段表 字符串段 debug段)。调试的时候需要debug段,必须把debug信息加载到内存上。
3 把可执行文件的入口地址写到CPU的PC寄存器里面(运行这个程序,CPU就知道运行这个可执行文件的第一条指令。在程序请求执行的时候,操作系统的加载器loader程序已经把文件的文件头里面的入口地址Entry point address是main函数第一行的地址放到cpu的PC寄存器里面。所以等到这个进程得到CPU资源的时候,CPU就知道从这个地址开始执行指令)

3.5 函数调用堆栈

int sum(int a, int b)
{
	int temp = 0;//定义局部变量
	temp = a + b;
	return temp;
}
int main()
{
	int a = 10, b = 20;//局部变量是指令:访问局部变量都是通过ebp指针的偏移量。
	//修改 a值的汇编指令
	//_asm{mov  dword ptr[a], 30h}//30h是48
	//_asm{mov  dword ptr[ebp-4], 30h}//30h是48      这样出错了!!!!!!

	int ret =0;
	ret= sum(a, b);//一个函数的调用,先压实参(实参压栈),之后再调用这个函数。实参的入栈顺序是从右向左入的。因为:C和C++要支持可变参函数。即参数个数不固定。从右向左 可以知道传入参数个数。
	/*实参入栈的这两块内存是:sum函数的两个形参的地址。
	形参内存的开辟:是在调用sum函数的时候,压实参时候开辟的。是在主调方回退。
	回退栈帧:没有什么清零,只是把sum函数栈帧的esp栈底指针回退到其栈底指针ebp上。数据还存在的,但不一定每次都能访问到。然后出栈 把esp向下移动,把栈顶元素赋给ebp。这个栈底元素是调动方的栈底地址。
	*/
	printf("%d \n", ret);
	 
	return 0;
}

对于上面的代码,有下列几个问题:
1 函数调用函数,栈帧开辟及回退过程是什么?
2 在main函数执行过程中,调到sum函数执行代码段时,中间做了什么事情?
3 在sum函数运行完,怎么知道回到main函数的?
4 当回到main函数后,是怎么知道继续从下一行指令继续运行的?

两种汇编:
inter的X86:从左往右看;
AT&T unix 从右往左看

调试反汇编之后会发现我们函数中
在进入sum函数后,每一个函数进来的这一段汇编指令做了三件事情:
1 push ebp:把主调方函数的栈底地址入栈,记录在当前函数的栈底内存里面
2 mov ebp,esp:让ebp指针指向当前函数的栈底
3 sub esp,0CCh 通过esp的减等操作给被开辟函数开辟栈帧,接下来rep stos相当于for循环,循环拷贝把ebp esp之间的所有的栈内存初始化为0CCCCCCCCh(一个固定值,是无效值 不是随机值)。

每次拷贝四个字节,总共ecx 33h次,所以总共CCh即:sub esp,0CCh。即栈帧的初始化

把30放到局部变量temp内存里面。return temp;
是把ebp-4即temp这个内存里的值放到一个eax寄存器里面,返回一个整型值并没有产生临时量,而是通过eax一个四字节的寄存器就把值带出来了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zYg8rXGY-1610878203193)(C:\Users\lhh31\AppData\Roaming\Typora\typora-user-images\1610877755330.png)]

sum函数运行结束:栈帧的回退:
直接把esp从栈顶拉到栈底。回退栈帧:栈帧没有进行清零,数据还存在
pop ebp;出栈:从栈顶出。把栈顶元素0x100给ebp,因此 ebp又重新指向栈底地址,而且栈底地址 esp向下移动即esp指向原来PC寄存器的值。

ret指令两个动作: 出栈;其栈顶元素(是当时调用函数中call指令的下一行指令地址)赋给CPU的PC寄存器。此时的esp指向了a=10;
原来eax存放的是temp的返回值,现在给ret
而main函数栈帧 的栈底存放ebp的是调用main函数的主调函数 mainCrTStartup()栈底地址。

1 ebp指栈底指针(高地址) esp指栈顶指针(低地址),他们是寄存器,表识栈的区域。
2 在函数中的局部变量a b ret在汇编指令上,在栈上访问他们的内存时,其实没有这三个的名字。函数里的局部变量是指令:在栈上访问局部变量都是通过ebp栈底指针的偏移量。函数在运行的时候,在栈上开辟内存,在栈内存上访问栈上的变量和数据都是通过ebp栈底指针的偏移量。
3 一个指针指向一块内存,对于32位CPU来说,CPU的位数是其ALU的宽度。32的寄存器都是32位,4个字节(地址就是用十六进制表示的四字节的整数值)。int* 指向一个int。
4 mov是移值的。dword是4字节。eax是4字节寄存器。lea是移地址的。sub减等。add加等。
把10(0Ah)放到 ebp-4的所表示的四个字节的内存里面。
5 一个函数的调用,先压实参(实参压栈),之后再调用这个函数。实参的入栈顺序是从右向左入的。因为:C和C++要支持可变参函数。即参数个数不固定。从右向左 可以知道传入参数个数。
push是入栈:从栈底入。b的值入栈之后,栈底指针esp是要向上移动四个字节。

重点1: 实参入栈的这两块内存是:sum函数的两个形参的内存。
形参内存的开辟:是在调用sum函数的时候,压实参时候会push时栈上开辟的。形参内存是主调函数中在实参压栈的时候也即形参初始化时。形参内存也是在主调方回退。
重点2: call指令:跳转到sum函数去执行了。但是回来main函数栈帧上,是怎么记住自己回来以后,继续从哪开始运行?
在进入sum函数之前:call指令做两件事:1 把下一行指令的地址先入栈(在main函数的栈帧上)2
回退栈帧:没有什么清零,只是把sum函数栈帧的esp栈底指针回退到其栈底指针ebp上。数据还存在的,但不一定每次都能访问到。然后出栈 把esp向下移动,把栈顶元素赋给ebp。这个栈底元素是调动方的栈底地址。
注:符号的重定向:在编译阶段,符号是不分配地址的,是因为:我们当前文件有可能引用了外部的符号,编译过程中是独立编译的看不见外部符号的定义,只有在链接的时候第一步进行合并符号表,符号解析,解析正确之后,才给符号分配内存地址。也即在编译阶段,所有的汇编指令代码上引用符号的地方全部不是合法的地址:数据是0地址,函数符号地址是-4。在符号解析后,得到每一个符号的虚拟地址以后,回头在我们的指令段上数据符号的地址填成数据符号的绝对的虚拟地址,函数符号的地址填成跟下一行指令的偏移量,因为CPU去执行指令是根据PC里面存的地址。

在进入sum函数之前:call指令做两件事:1 把下一行指令的地址先入栈(在main函数的栈帧上),然后栈顶指针esp继续向上移动。2
整个过程的总结:
1 把主调函数 mainCrTStartup()栈底地址压到main函数的栈帧上,此时的ebp还指向主调函数 mainCrTStartup()栈底地址;
2 把esp赋给ebp,即让ebp指向main函数的栈底地址;
3 sub esp,0E4h 即给main函数开辟栈帧;
4 然后把ebp esp之间的所有的栈内存初始化为0CCCCCCCCCh(一个固定值);
5 定义一个局部变量a 把A放到ebp-4的这个地方,局部变量b 把14放到ebp-8的这个地方,局部变量ret 把0放到ebp-C的这个地方;
6 对于函数调用先压实参(实参从右向左压栈),实参的入栈顺序是从右向左入的,先从内存里面ebp-8的这个地方把b的拿出来放到一个寄存器里 此寄存器入栈(push eax:就是给sum函数形参变量b分配内存),从内存里面ebp-4的这个地方把a的拿出来放到一个寄存器里 此寄存器入栈(相当于给sum函数形参变量a分配内存);
7 call指令 不能直接进入sum函数,先把下一行指令的地址先入栈(在main函数的栈帧上),进入被调用函数;

8 首先把主调函数 main()栈底地址压到sum函数的栈帧栈底地址上,esp上移,此时的ebp还指向主调函数 main栈底地址
9 把esp赋给ebp,即让ebp指向当前sum函数的栈底地址;
10 sub esp,0CCh esp减等数值即给sum函数开辟栈帧;
11 然后把ebp esp之间的所有的栈内存初始化为0xCCCCCCCCh(一个固定值 无效值);
12 定义一个局部变量temp 把0放到ebp-4的这个地方;
13 b在ebp+0Ch处,mov eax,dword ptr [a] a在ebp+08h处 add eax,dword ptr [b]加起来放到eax寄存器里,mov dword ptr [temp],eax 然后把eax寄存器的值给局部变量temp 放到ebp-4的这个地方;
14 return temp; mov eax,dword ptr [temp] 把局部变量temp值给eax寄存器,没有产生临时量。
15 大括号} mov esp,ebp 把ebp值赋给esp 即把esp拉到ebp处。回退sum函数的栈帧内存
16 栈顶 pop ebp 。把栈顶元素出栈,并把栈顶元素赋给ebp。esp向下移动。即ebp指向调用函数main的栈底地址 回来了
17 ret指令 两个动作: 出栈,其栈顶元素(是当时调用函数中call指令的下一行指令地址)赋给CPU的PC寄存器。esp向下移动。
18 call指令的下一行指令地址 add esp,8 其作用:回退形参变量的栈内存 此时esp真真正正回到main函数的栈顶
19 mov dword ptr [ret],eax 把eax寄存器值放到ret 即ebp-C的这个地方;ret=30;

第三部分笔记参考:https://rng-songbaobao.blog.csdn.net/article/details/90143576
视频参考:https://www.bilibili.com/video/BV1xf4y127AJ?p=2&t=5984
书籍参考:程序员的自我修养:链接 装载和库

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值