编译链接原理
1)(.cpp文件) 预编译 ( 生成 .i文件)
1. 将所有的 "#define" 删除 ,并展开所有的宏;
2. 处理掉所有条件预编译指令;如:"#if" ,"#ifdef" ,"#elif" ,"#else", "#endif" ;
3. 处理 “#include" 指令 (递归过程);
4. 删除所有的注释 : " // " ''/* */'' ;
5. 添加行号和文件名标识 ;
6. 保留所有的 #pragma 编译器指令 ,待编译器使用 ;
2)编译(生成.s文件)
1.词法分析
2.语法分析
3.语义分析
4.代码优化
5.生成汇编指令
3)汇编(生成.o文件,也叫目标文件)
1.翻译指令
将汇编代码转变成机器可以执行的指令,
每一个汇编语句几乎都对应一条机器指令。
4)链接(生成.exe文件,也叫可执行文件)
1.合并段和符号
2.符号解析
3.分配地址和空间
4.符号重定位
1.弱符号:全局的未初始化的符号
强符号:全局的已初始化的符号
链接阶段链接器只关注符号表中的全局符号
强弱符号的选取规则:
1. 两个强符号 编译报错
2. 一个强符号 一个弱符号 选择强符号
3. 两个弱符号 根据不同编译器处理方式不同
2.符号表: 外部符号
符号解析:在每个文件符号引用(引用外部符号)的地方找到符号的定义;
符号:存放在.data和.bss段的变量都要有一个名称来标识这个变量,这个名称称为符号
符号重定向:就是对.o文件中.text段指令中的无效地址给出具体的虚拟地址或者相对位移偏移量。
3.指令段:虚假的地址和偏移
虚拟地址空间
1.虚拟地址空间
1.创建内核映射结构体,并建立虚拟地址空间和物理内存的映射;
2.加载指令和数据;
3.把入口地址写入下一行指令寄存器
2.虚拟地址空间布局:(4G,ALU宽度)
栈区:局部变量、函数栈帧、形参变量;
堆区:malloc 或 new 开辟的内存;
数据段:全局变量、静态全局变量,静态局部变量;
(.data:已初始化,或初始化值不为0;.bss:未初始化或初始化值为0)
text段:指令
.rodata段:常量字符串,如:"hello word";
栈区和堆区的区别:
栈区内存:
1.由系统自动分配,系统自动释放,以函数为单位进行栈内存分配;其操作方式类似于数据结构中的栈;
2.内存分配释放 速度快效率高,内存连续;
3. 栈是由高地址向低地址扩展的连续内存;
4. 在 linux 系统上大家是可以通过 ulimit -s 来查看默认的栈大小的,
linux中栈的默认大小是 10M,windows 系统默认的栈大小是 1M;
堆区内存:
1.堆:需要程序员自己申请,并指明大小。
在c中malloc函数如p1 = (char *)malloc(10);
在C++中用new运算符,但是注意p1、p2本身是在栈中的,因为他们还是可以认为是局部变量。
若程序员不释放,程序结束时可能由OS回收 , 分配方式类似于链表;
堆内存需要用户自己管理,因此堆内存容易造成内存泄漏;
2.堆内存的分配释放相对栈来说效率低一些,内存不一定连续,容易产生内存碎片,但灵活性高;
3.堆是由低地址向高地址扩展的非连续内存,影响堆大小的因素比较多,和系统有效虚拟内存的大小有关;
4.堆空间的大小和当前系统的物理内存大小,交换分区大小,栈的大小,所使用共享库的大小都息息相关,
因为它们都属于用户空间部分,x86 32bit linux 系统默认给用户空间是 3G,
当我们在代码上分配空间的时候,不管是栈还是堆,其实都只是在虚拟地址空间上分配的内存空间,
当真正进行读写使用的时候,随着不断的发生缺页异常,才会去分配真正的物理内存和虚拟地址空间
上的页面进行映射。缺页异常处理函数在内核上是 do_page_fault,
越界访问栈空间,内核会在一定范围对栈的空间进行增长的。
内存分配方式有几种
三种 1.数据段 2.栈 3.堆
数据段(静态存储区域)分配:
内存在编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
在栈上创建:
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
在堆上分配:
亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,
程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,
但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏;
频繁地分配和释放不同大小的堆空间将会产生堆内碎块;
全局变量和局部变量是有什么区别,是怎么实现的
全局变量是整个程序都可以访问的,生存周期从程序运行开始到运行结束;
局部变量只有子模块可以访问,生存周期从定义点到子模块结束;
全局变量在数据段,程序运行前被加载;
局部变量在堆栈上,运行时分配内存;
操作系统和编译器通过内存分配的位置来实现的,
全局变量分配在全局数据段并且在程序开始运行的时候被加载;局部变量则分配在堆栈里面 ;
1.从作用域看:
全局变量:其作用范围是“整个工程”,只需在一个源文件中定义,就可以作用于所有的源文件。
当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量;
静态全局变量:使用 static 关键字修饰,也具有全局作用功能,和全局变量区别在于如果该程序包含多个文件,
其作用范围仅在定义的那个文件,不能作用于其它文件,
这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量;
局部变量:仅仅从定义的位置开始,到定义它的右花括号结束,只在函数执行期间存在,
函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回;
静态局部变量:局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,
和局部变量的区别在于函数执行完也还存在;
2.从生存期看:
全局变量: 随进程持续性
静态全局变量: 随进程持续性
局部变量: 从定义开始到函数结束,函数调用后变量就被撤销,内存被回收
静态局部变量: 随进程持续性,static 修饰的局部变量其生存期从函数变为整个进程
3.从内存分配看:
全局变量: 全局(静态)存储区 //.data .bss
静态全局变量: 全局(静态)存储区 // .data .bss
局部变量: 存放在栈中,只有在所在函数被调用时才动态地为变量分配存储单元 //.stack
静态局部变量: 全局(静态)存储区 //.data
举举例说明全局变量的优缺点 ?
优点:
1)全局可见,任何 一个函数或线程都可以读写全局变量-同步操作简单。
2)内存地址固定,读写效率比较高。
缺点:
1)全局变量存放在静态存储区,系统需要为其分配内存,一直到程序结束,才会释放内存,
这一点就局部变量的动态分配,随用随从栈中申请,用完(函数调用完毕)就释放。
2)影响函数的封装性能:我们肯定是希望我们写的函数具有重入性,就如一个黑盒子一般,
只通过函数参数就能得到返回,内部实现要独立,但是如果函数中使用了全局变量,
这势必就破坏了函数的封装性,会造成对全局变量的依赖。
3)降低函数的移值性,原因同上。
4)降低代码的可读性,这也意味着系统维护会不方便,因为一个全局变量可能会出现程序中的各个环节,
函数的执行也会根据环境变化而变化,所以调试会不太方便。
5)全局变量的读写,可能会延迟,这主要是体现在“写”操作上,由于写操作,一般需要2个周期操作,
所以有可能会出现,这边没写完时,那边已经读了,结果读到的不是最终值,这个是一个概率事件,
概率很小,但是并不代表没有。
寄存器
1. ebp 栈底指针寄存器
2. esp 栈顶指针寄存器
3. ###PC 下一行指令寄存器
4. ELF Linux的ELF格式的文件解析器
返回值:
<= 4 eax
<=8 >4 eax edx
>8 临时量
32位通用目的寄存器的指定用途如下:
eax : 累加器
ecx:计数器
edx:I/O指针
ebx:DS段的数据指针
esp:堆栈(stack)指针
ebp:SS段的数据指针
esi:字符创操作的源指针:SS段的数据指针
edi:字符创操作的目标指针:ES段的数据指针
汇编就是“寄存器与寄存器” 或者 “寄存器与内存” 之间来回移动数据;
函数的开栈
1.压入实参 ,自右向左;
2.压入下一行指令地址;
3.压入调用函数的栈底指针寄存器的值;
4.跳转到被调用方函数栈帧;
5.被调用方开辟局部变量活动的空间并初始化;(CCCC CCCC 内核地址)
调用约定
_cdecl C标准调用约定
_stdcall Windows标准调用约定
_fastcall 快速调用约定
_thiscall 类成员方法调用约定,依赖对象调用;
作用:
1.函数符号的生成
2.实参的入栈顺序 ==> 自右向左
3.形参的开辟,清理方式
_cdecl 调用方开辟,调用方清理
_stdcall 调用方开辟,被调用方清理
_fastcall 两个参数 ,寄存器
_stdcall 两个参数 ,处理方式相同