在前面我们提到过栈的概念,栈是一段特殊的内存区域,对于刚入门的汇编程序员来说,栈的概念经常容易被误解,下面就详细的描述栈和栈操作相关的汇编指令...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第150页到第157页 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为157 / 577)。

The Stack (栈):

    在前面我们提到过栈的概念,栈是一段特殊的内存区域,对于刚入门的汇编程序员来说,栈的概念经常容易被误解,下面就详细的描述栈和栈操作相关的汇编指令。

How the stack works (栈是如何工作的):

    栈之所以特殊是因为它的数据存取方式与众不同,前面提到过的data数据段,里面的数据是从低地址往高地址存放,而栈里的数据则刚好相反,它从高地址往低地址存放数据,如下图所示:

图1
   
    从图中memory addresses的箭头方向可以知道,下方是内存低地址(如图中最低内存地址存放了字节12),上方是内存高地址,前面说了栈增长的方向是和内存地址增长的方向是相反的,ESP寄存器指向的是栈顶,对应的就是栈内存区域的最低内存地址(就是图中最下方的内存位置),那么栈底对应的就是栈内存区域的最高内存地址(就是图中最上方的内存位置)。

    可以看到栈底有一段existing stack data部分,该部分里存放的是程序启动时,操作系统存放在里面的一些和程序有关的信息,比如需要传递给程序的命令行参数等。

    current ESP指向的位置就是程序启动时栈顶的位置,current ESP上方的区域就是existing stack data部分,它的下方区域将存放程序代码里压栈的数据。

    所谓压栈就是往栈中存放数据,代码中可以通过push指令来完成压栈操作,如上图所示,代码里有个pushl %ecx的指令,该指令就会将ECX寄存器里的值压入栈,可以看到压栈操作完成后,current ESP位置向下移动到了new ESP的位置,new ESP指向了新的栈顶,并且指向了新压入栈的数据,由ESP的移动方向就证明了栈里数据增长方向是向下的,也就是从高地址往低地址方向增加数据。

    栈里面除了开头存放的是操作系统存放的信息外,其余部分都是你的代码中存放的数据,栈里的这些数据有的可以用作局部变量,有的可以用作函数之间传递参数。你当然可以通过MOV之类的指令手动往栈中存放数据,但是那样你就需要自己修改ESP栈顶指针,如果没修改ESP指针就会导致程序出错,另一种更好的方式就是用现成的PUSH和POP指令来进行压栈和弹出栈操作,这些指令会自动帮你修改栈顶指针,下面就具体的描述下这两种栈操作指令。

PUSHing and POPing data (压栈和弹出栈):

    往栈中存放新的数据叫做压栈,对应的指令为PUSH ,PUSH指令的格式如下:

pushx source

    x字符是一个通配符,用于表示压入数据的尺寸,有两种可以压入的尺寸:
  • 当 x 为 l 时,表示32位大小
  • 当 x 为 w 时,表示16位大小
    MOV指令也有类似的后缀,不过PUSH指令只有这两种大小。

    source是要压入栈中的数据,source可以是以下几种数据类型:
  • 16-bit register values  
    16位的寄存器值
  • 32-bit register values 
    32位的寄存器值
  • 16-bit memory values 
    16位的内存值
  • 32-bit memory values 
    32位的内存值
  • 16-bit segment registers 
    16位的段寄存器
  • 8-bit immediate data values 
    8位的立即数
  • 16-bit immediate data values 
    16位的立即数
  • 32-bit immediate data values 
    32位的立即数
    下面是PUSH指令的例子:

pushl %ecx # puts the 32-bit value of the ECX register on the stack
pushw %cx # puts the 16-bit value of the CX register on the stack
pushl $100 # puts the value of 100 on the stack as a 32-bit integer value
pushl data # puts the 32-bit data value referenced by the data label
pushl $data # puts the 32-bit memory address referenced by the data label

    注意:代码中data和$data是不同的含义,data表示data标签引用的内存位置里的值,而data添加$符号后即$data,则表示data所引用的内存地址,一个是内存地址,一个是内存地址里包含的值,所以在用的时候不要混淆了。

    PUSH指令是往栈中存放数据,如果要从栈中获取数据,可以使用POP指令。

    POP指令的格式如下:

popx destination

    x依然是表示弹出数据尺寸大小的通配符,x为 l 时表示32位大小,x为 w 时表示16位大小。

    destination目标操作数表示数据弹出栈后,用于接收数据的元素,destination可以是以下几种类型:
  • 16-bit registers  16位的寄存器
  • 16-bit segment registers  16位的段寄存器
  • 32-bit registers  32位的寄存器
  • 16-bit memory locations  16位的内存位置
  • 32-bit memory locations  32位的内存位置
    下面是POP指令的简单例子:

popl %ecx # place the next 32-bits in the stack in the ECX register
popw %cx # place the next 16-bits in the stack in the CX register
popl value # place the next 32-bits in the stack in the value memory location

    有关PUSH和POP的完整例子,可以查看下面的pushpop.s程序:

# pushpop.s - An example of using the PUSH and POP instructions
.section .data
data:
    .int 125

.section .text
.globl _start
_start:
    nop
    movl $24420, %ecx
    movw $350, %bx
    movb $100, %eax
    pushl %ecx
    pushw %bx
    pushl %eax
    pushl data
    pushl $data

    popl %eax
    popl %eax
    popl %eax
    popw %ax
    popl %eax
    movl $0, %ebx
    movl $1, %eax
    int $0x80

    尽管上面是个简单的程序,但是通过调试这个程序就可以了解栈的工作方式,通过调试可以发现,ESP栈顶指针寄存器的值会随着数据的添加而递减,并且在每次向栈中添加新数据后,ESP都会指向该数据,这表明栈在内存中确实是向下移动的(即从高地址向低地址移动)。

    下面是程序刚启动时,调试器输出的ESP寄存器的值:

(gdb) print/x $esp
$1 = 0xbffffd70

    当所有PUSH指令都执行完后,ESP寄存器的值变为:

(gdb) print/x $esp
$2 = 0xbffffd5e

    将两个地址相减,得到的值18就是程序中5个PUSH指令压入栈的数据的总字节大小。

    POP指令操作和PUSH刚好相反,POP在将ESP指向的栈顶数据弹出到目标操作数后,还会将ESP的值加上弹出数据的尺寸大小,这样一个POP指令执行后,ESP就会指向前一个压入栈中的数据,那么下一次POP时,就可以把前一次的数据给弹出去了。所以POP指令会让栈在内存中向上移动(即从低地址向高地址移动)。当示例程序中5个POP指令都执行完后,ESP寄存器的值又会回到程序启动时的值。

PUSHing and POPing all the registers (压入和弹出所有寄存器):

    如果想通过单一的指令一次将所有的通用寄存器或标志寄存器给压入或弹出栈,可以使用如下指令:

Instruction
指令
Description
描述
PUSHA/POPA Push or pop all of the 16-bit 
general-purpose registers 
将所有16位通用寄存器压入或弹出栈
PUSHAD/POPAD Push or pop all of the 32-bit 
general-purpose registers 
将所有32位通用寄存器压入或弹出栈
PUSHF/POPF Push or pop the lower 16 bits of 
the EFLAGS register 
将EFLAGS标志寄存器的低16位数据压入或弹出栈
PUSHFD/POPFD Push or pop the entire 32 bits of 
the EFLAGS register 
将EFLAGS标志寄存器的所有32位数据压入或弹出栈

    PUSHA和PUSHAD会将所有通用寄存器的值压入栈中,PUSHAD会按以下顺序压入栈:EAX、ECX、EDX、EBX、ESP(原始值)、EBP、ESI 及 EDI ,PUSHA指令对应压入16的寄存器,也是相同的压栈顺序:AX、CX、DX、BX、SP(原始值)、BP、SI 及 DI ,可以用下面的等效语句来加深理解:

IF OperandSize <- 32 (* PUSHAD instruction *) THEN
    Temp <- (ESP);
    Push(EAX);
    Push(ECX);
    Push(EDX);
    Push(EBX);
    Push(Temp);
    Push(EBP);
    Push(ESI);
    Push(EDI);
ELSE (* OperandSize <- 16, PUSHA instruction *)
    Temp <- (SP);
    Push(AX);
    Push(CX);
    Push(DX);
    Push(BX);
    Push(Temp);
    Push(BP);
    Push(SI);
    Push(DI);
FI;

    POPA和POPAD指令则会以压栈相反的顺序来弹出数据到对应的寄存器中,对应的等效语句如下:

IF OperandSize 32 (* instruction POPAD *) THEN
    Pop(EDI);
    Pop(ESI);
    Pop(EBP);
    increment ESP by 4 (* skip next 4 bytes of stack *)
    Pop(EBX);
    Pop(EDX);
    Pop(ECX);
    Pop(EAX);
ELSE (* OperandSize 16, instruction POPA *)
    Pop(DI);
    Pop(SI);
    Pop(BP);
    increment ESP by 2 (* skip next 2 bytes of stack *)
    Pop(BX);
    Pop(DX);
    Pop(CX);
    Pop(AX);
FI;

    上面等效语句中,都跳过了栈中压入的原始的4字节ESP或2字节SP数据,因为不可能用原来的栈顶指针来覆盖掉当前的ESP栈顶指针,覆盖的话就没办法进行后面的POP操作了,因为ESP都变了,弹出的数据肯定就不对了。

    POPF和POPFD指令会根据处理器的操作模式产生不同的行为,当处理器运行在保护模式下ring 0即特权级别为0时,EFLAGS寄存器中除了VIP , VIF 及 VM标志外,其他所有非保留的标志位都能被修改,而VIP , VIF标志位会被清零,VM标志位不受影响。

    当处理器运行在保护模式下,如果ring即特权级别大于0但小于或等于IOPL时,则除 IOPL , VIP、VIF 以及 VM 标志之外的所有非保留标志都可以修改。而IOPL标志不受影响,VIP 与 VIF 标志会被清零,VM 标志不受影响。

    上面涉及到的EFLAGS寄存器中的标志位的含义在前面的章节:IA-32平台(三) 硬件介绍结束篇及各种处理器平台 都做了介绍。

Manually using the ESP and EBP registers (手动使用ESP和EBP寄存器):

    除了可以使用PUSH和POP指令操作栈中的数据外,你还可以直接使用ESP作为内存指针,通过MOV之类的指令手动将数据放置到栈中。

    不过一般情况下都是将ESP寄存器里的值拷贝到EBP,然后使用EBP指向函数栈的基址,通过EBP来访问函数在栈中的参数等数据,有关这方面的更多信息将在后面描述函数的章节中进行详细介绍。

Optimizing Memory Access (优化内存访问):

    内存访问的操作可以说是处理器中最慢的操作了,如果你想写出高效的汇编程序,就应该尽量避免过多的内存访问,并尽可能的将数据存储到处理器的寄存器中,操作寄存器可比操作内存要快得多了。

    当然你不可能将所有数据都放到寄存器中,因为寄存器的数目是有限的,那么你就应该在内存访问方面例如数据定义方面做优化。

    大多数处理器尤其是IA-32平台的处理器,都是通过缓存的方式来读取内存数据的,它们以data段的起始地址为起点,每次从内存中读取一个缓存块,在奔腾4处理器上,缓存块的大小是64位,所以如果你定义了一个数据元素,而这个数据元素跨越了64位的块边界,那么就会导致处理器需要花两个缓存操作来读取这个数据。

    所以为了避免不必要的读取开销,英特尔建议使用下面的规则来定义数据:
  • Align 16-bit data on a 16-byte boundary
    对齐16位数据时,应该把它放置到16字节边界上
  • Align 32-bit data so that its base address is a multiple of four
    对齐32位数据时,让它的基地址是4的倍数
  • Align 64-bit data so that its base address is a multiple of eight
    对齐64位数据时,让它的基地址是8的倍数
  • Avoid many small data transfers. Instead, use a single large data transfer
    尽量使用一整块连续的数据来代替许多分散的小数据
  • Avoid using larger data sizes (such as 80- and 128-bit floating-point values) in the stack
    尽量避免在栈中使用80位或128位浮点数之类的大尺寸数据。
    另外,尽量将字符串等没有固定尺寸的数据放在data段的末尾,这样之前数据的对齐就不会受到影响。

    gas汇编器支持.align伪操作符,使用该伪操作符可以对内存数据进行对齐,.align应放在data段中数据定义之前,具体的信息会在后面的章节中进行介绍。

    剩下就是一些总结部分,这里就不多说了,本篇翻译完英文原著的第5章。下一篇进入原著第6章,将介绍汇编程序流程控制方面的指令。

    OK,到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 汇编流程控制 (一)

上一篇: Moving Data 汇编数据移动 (三) 数据交换指令

相关文章

汇编字符串操作 (二) 字符串操作结束篇

汇编里使用Linux系统调用 (一)

使用内联汇编 (一)

汇编开发相关工具 (一)

IA-32平台(三) 硬件介绍结束篇及各种处理器平台

汇编函数的定义和使用 (一)