一、虚拟内存布局、编译链接原理等基础概念
一、实模式和保护模式
实模式和保护模式都是CPU的工作模式,而CPU的工作模式是指CPU的寻址方式、寄存器大小等,用来反应CPU在此模式下如何工作。
(一)实模式
【1.概念:】
实模式出现于早期8088CPU时期。当时由于CPU的性能有限,一共只有20位地址线,地址空间为220=1MB,所以 程序中用到的地址都是真实的物理地址,采取分段映射直接将程序加载到物理内存中,如下图所示:
假如A是一个恶意程序,它加载到真实地物理空间后,就可以通过指针+偏移 访问物理地址中所有程序,这样就存在其他程序指令或数据被修改的风险,导致其他程序被恶意篡改。
【2. 特点:】
- 实模式开创性的提出了 地址分段的概念。
- 实模式 存在安全隐患,实模式下对地址的访问是物理地址,那么程序可以任意修改物理地址,甚至包括操作系统所在的内存,这就给操作系统带来了极大的安全问题。
(二)保护模式
【1.概念:】
Intel 80386以后的X86系列处理器,它的地址总线和寄存器都是32位的,因此其单寄存器的寻址空间扩大到了4GB,为了保护操作系统,有了保护模式的这个概念,它引入了虚拟内存 ,程序中看到的地址是逻辑地址,需要经过虚拟地址的映射才可以得到真正的物理地址。如下图所示:
假如A现在是一个恶意程序,这时它看到的地址是逻辑地址(虚拟地址),如果它再利用指针+偏移进行破坏,也只能在A对应的虚拟地址空间范围内移动,因为真实的物理地址需要通过页表映射或段映射才能确定,这个地址恶意程序看不到,故就不能修改其他进程 。这样就实现了对其他进程的保护,提高了安全性。
【2. 特点:】
- 保护模式可以采取 分页映射,也可以采取分段映射。
- 当程序运行在保护模式下的时候,某个特定的程序要访问相应的内存的时候将要访问的地址交到操作系统的手中,然后操作系统会对要申请的地址进行进行页表映射转换最后返回转换后的地址供程序使用,这样 保证了系统的安全性。
二、4G虚拟内存空间布局
进程地址空间需要隔离,防止恶意的程序修改其他程序的内存数据,因此计算机中引入虚拟地址空间。32位,64位操作系统根据一次最多能处理的字节数来判断即算术逻辑单元ALU的宽度来判断。
当操作系统x86为32位时,其内存为:232=4G,故虚拟地址空间为4G。虚拟地址空间分为:
- 用户空间:应用程序可以使用的空间。
- 内核空间:只有操作系统才可以操作的空间。
Linux默认以3:1的比例分配;Windows默认以2:2划分,都可以自己改变。
我们现在看一下操作系统x86为32位的4G虚拟内存空间布局,注意和内存空间布局区分,如下图所示:
【1. 用户空间:】
-
保留区:【0x0000 0000~0x0804 8000】:128M大小,为保留区,不可访问,不允许读写。作用是:存放C基本的库实现,由操作系统调用。任何普通程序对它的引用都是非法的,一般用来捕捉空指针,在定义指针时将指针初始化为NULL,使其指向这个地址,就不会被引用了,从而不会出现野指针的问题。
-
指令段【.text】:存放指令;可读,可执行。
-
数据段【.data】:存放程序中已初始化且初始化不为0的全局变量或静态变量;可读,可写。
-
数据段【.bss】:存放未初始化或初始化为0的全局变量或静态变量;可读,可写
-
堆【.heap】:存放动态数据,手动申请手动释放;动态开辟,自下向上增长。
-
共享库【libc.so】:在堆栈之间存放库函数的定义点,因为现在只有库函数的声明,所以必须获取函数的定义点,这时候就需要共享库的存在。
-
栈【.stack】:所有函数的活动空间,局部变量,包含局部变量等。从上向下增长。
-
命令行参数: 保存传递给main函数的参数,如./main “hello",此时的”hello"为命令行参数。
-
环境变量: 通过环境变量可以设置库的路径。
【2. 内核空间:】
- 内存直接访问区ZONE_DMA:16M,不需要经过CPU的寄存器,加快了磁盘和内存之间的数据交换。
- 常用区ZONE_NORMAL: 892M,内核中最重要的部分**,存放页表,页面的映射,PCB就在这里。**
- 高端内存区ZONE_HIGHMEM:128M,存放大文件的映射,即内核中映射高于1GB的物理内存使用,64位操作系统没有该段。
我们可以看到4G内存中有几段随机偏移量,这个是为了保证每次系统运行程序时,堆栈位置是不一样的,从而保障了不能通过固定的地方访问到堆栈,保护了堆栈。我们画图时可画可不画。
三、编译,链接,运行原理
将一个程序变为一个进程需要5步:预编译,编译,汇编,链接,运行。
(一)预编译
【1. 命令:】
gcc -E main.c -o main.i //生成文本文件
【2. 处理:】
- 把所有#开头的都处理了。宏替换,删除宏定义并做替换
- 递归展开头文件,因为头文件嵌套
- 处理#if #enif等预编译指令
- 删除注释
- 添加行号和文件标识,可以准确定位行错误
- 保留#pragma指令,交给编译器,预编译器无法处理
(二)编译
【1. 命令:】
gcc -S main.i -o main.s (文本文件)
【2. 处理:】
- 词法分析
- 语法分析(语法树的建立)
- 语义分析
- 代码优化
- 生成汇编
(三)汇编
【1. 命令:】
gcc -c main.s -o main.o (二进制文件)
【2. 处理:】
翻译指令,把指令翻译为0,1码。汇编遗留的问题:(前提,预编译,编译,汇编,是以一个个编译单元编译的)
- 弱符号的处理,注意只有C有,C++没有
- 符号表:会将符号声明放在UND没有定义区。
- 指令段:外部符号的地址不确定,虚假地址或虚假偏移。(就相当于你在自己的文件中的位置,不代表你在所有文件中的位置)
(四)链接
【1. 命令:】
gcc -o main main.o (二进制文件)
【2. 处理:】
- 合并段和符号表
- 符号解析(在符号声明的地方,找到定义)
- 分配地址和空间
- 符号重定位
前面4步相当于让程序和虚拟地址空间进行了映射,但虚拟地址空间还没有和物理空间映射,这就是最后一步要做的事情。
(五)运行
【1. 命令:】
./main
【2. 处理:】
- 创建虚拟地址空间和物理内存的映射,创建内核映射结构体,即PCB,创建页目录,页表。
- 加载指令和数据.
- 将main函数入口地址写入下一行指令寄存器,开始运行程序。
加油哦!????。