当前位置: 首页 > 编程语言 > 汇编语言 > 正文

简明x86汇编语言教程(5)

时间:2010-01-10

3.4 串操作

我们前面已经提到,内存可以和寄存器交换数据,也可以被赋予立即数。问题是,如果我们需要把内存的某部分内容复制到另一个地址,又怎么做呢?

设想将DS:SI处的连续512字节内容复制到ES:DI(先不考虑可能的重叠)。也许会有人写出这样的代码:


NextByte:
mov cx,512
mov al,ds:[si]
mov es:[di],al
inc si
inc di
loop NextByte
; 循环次数

我不喜欢上面的代码。它的确能达到作用,但是,效率不好。如果你是在做优化,那么写出这样的代码意味着赔了夫人又折兵。

Intel的CPU的强项是串操作。所谓串操作就是由CPU去完成某一数量的、重复的内存操作。需要说明的是,我们常用的KMP算法(用于匹配字符串中的模式)的改进——Boyer算法,由于没有利用串操作,因此在Intel的CPU上的效率并非最优。好的编译器往往可以利用Intel CPU的这一特性优化代码,然而,并非所有的时候它都能产生最好的代码。

某些指令可以加上REP前缀(repeat, 反复之意),这些指令通常被叫做串操作指令。

举例来说,STOSD指令将EAX的内容保存到ES:DI,同时在DI上加或减四。类似的,STOSB和STOSW分别作1字节或1字的上述操作,在DI上加或减的数是1或2。

计算机语言通常是不允许二义性的。为什么我要说“加或减”呢?没错,孤立地看STOS?指令,并不能知道到底是加还是减,因为这取决于“方向”标志(DF, Direction Flag)。如果DF被复位,则加;反之则减。

置位、复位的指令分别是STD和CLD。

当然,REP只是几种可用前缀之一。常用的还包括REPNE,这个前缀通常被用来比较两个串,或搜索某个特定字符(字、双字)。REPZ、REPE、REPNZ也是非常常用的指令前缀,分别代表ZF(Zero Flag)在不同状态时重复执行。

下面说三个可以复制数据的指令:

助记符 意义
movsb 将DS:SI的一字节复制到ES:DI,之后SI++、DI++
movsw 将DS:SI的一字节复制到ES:DI,之后SI+=2、DI+=2
movsd 将DS:SI的一字节复制到ES:DI,之后SI+=4、DI+=4

于是上面的程序改写为

cld
mov cx, 128
rep movsd
; 复位DF
; 512/4 = 128,共128个双字
; 行动!

第一句cld很多时候是多余的,因为实际写程序时,很少会出现置DF的情况。不过在正式决定删掉它之前,建议你仔细地调试自己的程序,并确认每一个能够走到这里的路径中都不会将DF置位。

错误(非预期的)的DF是危险的。它很可能断送掉你的程序,因为这直接造成缓冲区溢出问题。

什么是缓冲区溢出呢?缓冲区溢出分为两类,一类是写入缓冲区以外的内容,一类是读取缓冲区以外的内容。后一种往往更隐蔽,但随便哪一个都有可能断送掉你的程序。

缓冲区溢出对于一个网络服务来说很可能更加危险。怀有恶意的用户能够利用它执行自己希望的指令。服务通常拥有更高的特权,而这很可能会造成特权提升;即使不能提升攻击者拥有的特权,他也可以利用这种问题使服务崩溃,从而形成一次成功的DoS(拒绝服务)攻击。每年CERT的安全公告中,都有6成左右的问题是由于缓冲区溢出造成的。

在使用汇编语言,或C语言编写程序时,很容易在无意中引入缓冲区溢出。然而并不是所有的语言都会引入缓冲区溢出问题,Java和C#,由于没有指针,并且缓冲区采取动态分配的方式,有效地消除了造成缓冲区溢出的土壤。

汇编语言中,由于REP*前缀都用CX作为计数器,因此情况会好一些(当然,有时也会更糟糕,因为由于CX的限制,很可能使原本可能改变程序行为的缓冲区溢出的范围缩小,从而更为隐蔽)。避免缓冲区溢出的一个主要方法就是仔细检查,这包括两方面:设置合理的缓冲区大小,和根据大小编写程序。除此之外,非常重要的一点就是,在汇编语言这个级别写程序,你肯定希望去掉所有的无用指令,然而再去掉之前,一定要进行严格的测试;更进一步,如果能加上注释,并通过善用宏来做调试模式检查,往往能够达到更好的效果。