Assembly 间接寻址及堆栈
间接寻址
间接寻址允许寄存器像指针变量一样运作。要指出寄存器像一个指针一样被间接使用,需要用方括号([])将它括起来。例如:
1 mov ax, [Data] ; 一个字的标准的直接内存地址
2 mov ebx, Data ; ebx = & Data
3 mov ax, [ebx] ; ax = *ebx
因为AX可以容纳一个字,所以第三行代码从EBX储存的地址开始读取一个字。如果用AL替换AX,那么只有一个字节会被读取。认识到寄存器不像在C中的变量一样有类型是非常重要的。到底EBX具体指向什么完全取决于使用了什么指令。而且,甚至EBX是一个指针这个事实都完全取决于使用的指令。如果EBX错误地使用了,通常不会有编译错误;但是,程序将不会正确运行。这就是为什么相比于高级语言汇编程序较容易犯错的原因之一。
所有的32位通用寄存器(EAX,EBX,ECX,EDX)和指针寄存器(ESI,EDI)都可以用来间接寻址。一般来说,16位或8位的寄存器是不可以的。
子程序的简单例子
子程序是代码中的一个的独立的单元,它可以使用在程序的不同的地方。换句话说,一个子程序就像一个C中的函数。可以使用跳转来调用子程序,但是返回会是一个问题。如果子程序要求能使用在程序中的任何地方,它必须要返回到调用它的代码段处。因此,子程序的跳转返回最好不要硬编码为标号。下面的代码展示了如何使用JMP指令的间接方式来做这件事。此指令方式使用一个寄存器的值来决定跳转到哪(因此,这个寄存器与C中的函数指针非常相似。) 下面使用子程序的方法来重写第一章中的第一个程序。
子程序get_int使用了一个简单,基于寄存器的调用约定。它认为EBX寄存器中存的是输入双字的储存地址而ECX寄存器中存的是跳转返回指令的地址。25行到28行,使用了ret1标号来计算返回地址。在32行到34行,使用了$运算子来计算返回的地址。$运算子返回出现$这一行的当前地址。$+7表达式计算在36行的MOV指令的地址。
这两种计算返回地址的方法都是不方便的。第一种方法要求为每一次子程序调用定义一个标号。第二种方法不需要标号,但是需要仔细的思量。如果使用了近跳转来替代短跳转,那么与$相加的数就不会是7!幸运的是,有一个更简单的方法来调用子程序。这种方法使用堆栈。
这两种计算返回地址的方法都是不方便的。第一种方法要求为每一次子程序调用定义一个标号。第二种方法不需要标号,但是需要仔细的思量。如果使用了近跳转来替代短跳转,那么与$相加的数就不会是7!幸运的是,有一个更简单的方法来调用子程序。这种方法使用堆栈。
堆栈
许多CPU都支持内置堆栈。堆栈是一个先进后出(LIFO)的队列。它是以这种方式组织的一块内存区域。PUSH指令添加一个数据到堆栈中而POP指令从堆栈中移除数据。移除的数据就是最后入栈的数据(这就是称为先进后
出队列的缘故)。
出队列的缘故)。
SS段寄存器指定包含堆栈的段(通常它与储存数据的段是一样)。ESP寄存器包含将要移除出栈数据的地址。这个数据也被称为栈顶。数据只能以双字的形式入栈。也就是说,你不可以将一个字节推入栈中。
PUSH指令通过把ESP减4来向堆栈中插入一个双字,然后把双字储存到[ESP]中。POP指令从[ESP]中读取双字,然后再把ESP加4.下面的代码演示了这些指令如何工作,假定在ESP初始值为1000H。
堆栈可以方便地用来临时储存数据。它同样可以用来形成子程序调用和传递参数和局部变量。
80x86同样提供一条PUSHA指令来把EAX,EBX,ECX,EDX,ESI,EDI和EBP寄存器的值推入栈中(不是以这个顺序)。POPA指令可以用来将它们移除出栈。