前言
由于笔者水平有限,随笔中难免有些许纰漏和错误,希望广大读者能指正。
一、各种地址之间的区分
笔者在刚开始学习汇编语言时,不是很能分清楚汇编地址、逻辑地址、物理地址、段地址、偏移地址、线性地址等概念,这对之后学习造成了不小了影响。在花费了一番功夫之后,终于理清楚了其中的异同,希望对初学者有一些帮助。
1.逻辑地址与物理地址
在x86处理器的实模式中,逻辑地址就是段地址:偏移地址,段地址必须能被16整除,偏移地址是相对于段起始地址的偏移。下面的代码用于访问位于0x7c00:0x4e中的字节
mov al [0x7c00:0x4e]
x86处理器内置了CS、DS、SS、ES四个段寄存器,在程序执行之前,必须设置好这两个寄存器的值。处理器的总线接口部件负责把逻辑地址转换为物理地址。
当处理器访问内存时,它把指令中的内存地址看成段内的偏移地址,而不是物理地址。比如如下代码:
add al [0x3f]
指令中源操作数[0x3f]是相对于DS寄存器中段地址的偏移量,至于为什么是DS,因为如果没有指定段地址,那么默认使用DS寄存器中的值作为段地址。
处理器把段寄存器中的地址左移四位(也就是乘以16),再将其和指令中的地址相加,就得到了物理地址。以访问0x7c00:0x4e中的字节为例,处理器实际上访问的是
0x7c00 * 16 + 0x4e = 0xaa84e
这里生成了5位的十六进制物理地址,也就是20位二进制物理地址,8086CPU是有20位地址线的,所以最多可以访问2^20BYTE=1MB的内存地址。
同样在不允许段之间重叠的情况下,每个段的最大长度是64KB,因为偏移地址也是16位的,从0000H到FFFFH。
2.汇编地址
我们编写好.asm汇编程序源文件,把它交给编译器进行编译时,为了支持段地址:偏移地址这种访问模式,编译器会把源文件整体上作为一个独立的段来处理,从零开始计算和跟踪每条指令的地址,称为汇编地址。
我们可以通过让编译器生成.lst文件来查看每条指令的汇编地址。接下来以nasm编译器为例进行说明。(图中代码来自李忠老师)
如上图所示,红色部分即为每条指令的汇编地址,我们可以看到汇编地址是从0开始计算的,蓝色部分为对应指令的机器码,最右侧为汇编源代码。此外,从图中我们看出注释是不占用汇编地址的。
当程序装入物理内存之后,汇编地址是在内存段中的偏移地址。当程序加载到物理内存之后,指令在段内的偏移地址和它在汇编阶段的汇编地址是相同的。
细心的读者可能会想到,如果一个程序的大小超过了64KB了,我们就不能仅仅只是使用一个段来存储程序了。为了解决这个问题,我们可以使用以下方法:
- 拆分程序
- 手动切换段
- jmp、call
拆分程序
我们可以把程序拆分为数据段、代码段、栈段,每个段使用不同的寄存器进行访问
手动切换段
我们可以修改段寄存器的值,来访问其它段。
jmp、call
对于需要频繁切换段的程序,我们使用jmp、call实现段之间的跳转。
3.线性地址
要了解线性地址,就需要先了解IA-32处理器的架构以及分页机制。这部分内容需要深入到保护模式内部。笔者在这里先挖个坑,以后补上线性地址这部分内容。初学者暂时不需要了解这部分的内容。
在这里简单地说一下线性地址。IA-32处理器支持多任务,在以上所说地分段模型下,由于每个任务地内存段是大小不一的,在任务结束运行后需要对它的内存段进行释放,这就导致了内存碎片化的问题。也就是说,可用的地址空间被分成了许多部分。
如果在这时需要运行一个新的任务,很有可能出现空闲的内存地址足够运行新任务,但是由于空闲的地址被碎片化了,导致没有一个合适的内存区域用来运行这个新任务。
为了解决这个问题,IA-32处理器支持分页功能,分页功能将物理内存空间划分成逻辑上的页。页的大小一般为4KB,通过使用页,可以简化内存管理。
当页功能未开启时,线性地址就是物理地址,这个地址由处理器的段部件生成。当页功能开启时,段部件产生的地址就不再是物理地址了,而是线性地址(Linear Address),线性地址还要经页部件转换后,才是物理地址。
启用页机制后,IA-32中的每个任务都有一个大小为4GB的虚拟地址空间,这个虚拟空间的地址,就是线性地址,经过转换,它变成了物理地址。