三块核心内容
进程的虚拟地址空间内存划分和布局
虚拟地址空间
任何的编程语言 产生两种东西:指令和数据
产生在磁盘上的可执行文件
将程序从磁盘上加载到内存当中 不可能直接加载到物理内存
X86 32位 Linux环境下的 加载到内存
Linux系统会给当前进程分配一个2^32次方大小的空间4G (虚拟地址空间)
它存在,你能看得见,他是物理的
它存在,你看不见,他是透明的
它不存在,你却能看见,他是虚拟的
它不存在,你也看不见,它被删除了
#include<iostream>
using namespace std;
int gdata1 = 10;
int gdata2 = 0;
int gdata;
static int gdata4 = 11;
static int gdata5 = 0;
static int gdata5;
//以上全是数据 会在符号表中产生符号
int main()
{
int a = 12; //mov dowrd ptr[a],0Ah 或mov dword ptr[ebp-4],0Ah 实际生成ebp-4 a知识方便理解编译器自行划定的
int b = 0;
int c;
// 以上不产生符号 产生三条MOV指令 指令在text段 在指令运行之后在stack上开辟物理空间
static int e = 13;
static int f = 9;
static int g;
// 静态数据变量 也存放在数据段 只会在程序第一次运行到它的时候初始化
return 0;
}
虚拟内存地址布局
函数调用堆栈详细过程
main调用函数 函数执行完后 怎么知道回到那个函数?
函数执行完 会到main后怎么知道从哪一行指令开始继续执行?
#include<iostream>
using namespace std;
int sum(int a, int b)
{
//大括号和第一行代码之间有指令生成
/*
push ebp 将main函数栈底指针压入栈 方便在开辟新栈帧后ebp移动 释放新栈帧后ebp可以指向原来的位置
mov ebp,esp 将esp的值赋值给ebp 使ebp指向esp所指向的位置
sub esp,4CH 给函数开辟栈帧空间
rep stos for循环 给栈帧初始化0XCCCCCCCC -858993460
(add += sub-=)
*/
int temp = 0; //mov dowrd ptr[ebp-4],0
temp = a + b;
/*
mov eax,dword ptr[ebp+0Ch]
mov eax,dword ptr[ebp+08h]
mov dword ptr[ebp-4],eax
*/
return temp;
/*
temp是一个局部变量 出不去
四个字节 不用产生临时量 直接使用寄存器将返回值带出
mov eax,dword ptr[ebp-4]
<= 4 eax
>4 && <=8 eax,edx
>8产生临时变量
*/
//最后一行代码和大括号右括号之间有代码生成
/*
mov esp,ebp esp从上面移动到ebp栈底的位置 回退栈帧把栈空间交还给系统,对栈上的数据并没有进行清空
pop ebp 出栈 并且把出栈的值赋值给ebp 出栈的是mian函数ebp的地址
ret 出栈操作 栈顶(call 下一行指令的地址) 把出栈的内容放入CPU的PC寄存器
pC寄存器 下一行指令执的位置 执行完当前指令 就去执行PC寄存器中的指令
*/
}
int* func()
{
int data = 10;
return &data;
}
int main()
{
int a = 10; //mov dword ptr[ebp-4],0Ah
int b = 20; //mov dword ptr[ebp-8],14h
//在函数代码指令上访问局部变量 都是通过ebp栈底指针的偏移来访问的
int ret = sum(a, b);
//一个函数的调用要先压参数入栈 从右向左
//形参变量的内存开辟是在调用方函数中开辟好的
/*
mov eax,dword ptr[ebp-8]
push eax
mov eax,dword ptr[ebp-4]
push eax
call sum
*/
/*
call sum
将call指令 下一行的指令地址压栈 sum结束后知道从那行指令继续运行
add esp,8
将两个压栈的形参变量占用的内存 交还给系统 至此esp和ecp归位
mov dword ptr[ebp-0Ch],eax
将return时放入寄存器中的数值 赋值给sum
*/
cout << "ret:" << ret << endl;
int* p = func(); //非法访问内存
/*
如果调用func2 会覆盖这个栈帧func1 导致下文打印语句出错
*/
cout << *p << endl;
return 0;
}
程序编译链接原理(Linux)
源代码
使用g++ -c -src.cpp生成.o文件
符号表
使用objdump -t -filename.o 查看文件符号表
0 C++函数符号 = 函数名字+参数列表
汇编器在把文件最终转换成二进制可重定位的目标文件的过程中会生成符号表
1.对于符号的定义
.text main main函数产生的符号 一个函数产生的符号就是一个函数的名字,
符号代表的函数的指令到时会放在代码段上
.data data 源文件中所定义的全局变量且初值不为0所以在data段
2.对于符号的引用
符号将来放到哪里时未知的
*UND*代码中用到了他们但是却不知道他们是怎么定义的
变量gdata和函数sum也产生符号了
最左边数字右侧的第一个字母
l local 可见性为当前文件
g global 可见性为所有文件
链接器只能看见g符号看不见l符号
3.注
静态变量
静态函数
只能是当前文件可见
可以在多个文件中定义名字相同的静态全局变量和静态函数
若是在多个文件中定义名字相同的普通的全局变量或者函数 就会发生冲突
由于gdata 和 sum没有使用静态修饰 所以他们具有全局可见性 使用字母 g
使用objdump -s filename.o打印常见的段
.o文件格式组成
使用readelf -h filename.o查看elf头
使用readelf -s filename.o 打印所有的段信息
AX alloc execute 分配内存可执行
使用objdump -S -fimename.o查看详情
int a = gdata
int b = data
不管访问自己或者其他位置的全局变量 符号都没有地址
call sum
函数的地址也是0
从高级源代码转到汇编是由编译完成的 编译的指令无法使用,以上文件无法执行 因为符号的地址不可能是0地址,编译过程中符号不分配地址
编译链接Tips
符号什么时候分配地址
链接过程第一步 符号解析
所有符号*UND*都能找到定义的地方 .text/.data
分配地址
然后去代码段指令上 正确填充符号地址
可执行文件也有段表和obj文件类似 所有符号都找到定义的地方
现在记录在那个段 执行时就要加载到对应的虚拟内存空间位置
可以通过修改入口点地址确定第一条执行的指令
可执行文件
ELF头部 (只读储存器段)(代码段)
段头部表 (只读储存器段)(代码段)将连续文件节映射到运行时存储器段
.init (只读储存器段)(代码段)
.text (只读储存器段)(代码段)
.rodata (只读储存器段)(代码段)
.data (读/写储存器段)(数据段)
.bss (读/写储存器段)(数据段)
.symtab 不加载到储存器的符号表和调试信息
.debug 不加载到储存器的符号表和调试信息
.line 不加载到储存器的符号表和调试信息
.strtab 不加载到储存器的符号表和调试信息
节头表 不加载到储存器的符号表和调试信息 描述文件文件节
可执行文件比目标文件多了一个段
progrma headers 放两个LOAD
提示操作系统程序运行的时候,需要加载什么东西去内存中
代码段/数据段
加载到内存后从哪里开始执行
文件头中入口地址(放入 PC 寄存器)
符号解析 1.所有对符号的引用,都要找到该符号定义的地方,
问题:1.符号未定义 没有在符号表中找到符号
2.符号重定义 在符号表中找到多个符号
符号解析成功后,给所有符号分配内存地址