《PC Assembly Language》Paul A. Carter

第2章-基础汇编语言Basic Assembly Language

2.1 使用整型(Working with Integers)

2.1.1 整型的表示(Integer representation)

unsigned integers;
signed integers:

2.1.2 符号扩展(Sign extension)

缩短数据长度(Decreasing size of data)

直接丢弃高位数据即可:

mov ax, 0034h ; ax = 52 (stored in 16 bits)
mov cl, al ; cl = lower 8-bits of ax

若该数据无法用更小的长度表示,这种转化就会出现错误。

对于无符号数,当丢弃的高位全为0时,转化就是正确的;
对于有符号数,丢弃的高位全为0或者全为1,且第一个不被丢弃的位与被丢弃的位的值一样时,转化才是正确的。

增加数据长度(Increasing size of data)

对于无符号数,直接在高位补0即可;
对于有符号数,若符号位为0,则高位补0,若符号位为1,则高位补1;

80386提供了许多数字扩展指令。

记住计算机并不知道数据是否有符号,这都需要程序员自己去考虑。

对于无符号数,可以直接用mov指令把0传入高位比特。
例如,将AL中的一个字节扩展为AX的一个字:

mov ah, 0 ; zero out upper 8-bits

但无法用mov指令将AX中的一个字长度的无符号数转化为EAX中的双字长度的无符号数。
这是因为没有办法直接访问EAX的高16位。

为解决此问题,80386提供了movzx指令,此指令接收两个操作数:第一个操作数(目的操作数,destination)是16-bit或者32-bit的寄存器;第二个操作数(源操作数,source)可以是8-bit或16-bit寄存器,也可以是1字节或1个字的内存。
另外此指令要求目的操作数的长度必须大于源操作数(多数指令要求两操作数长度相等)

例如:

movzx eax, ax ; extends ax into eax
movzx eax, al ; extends al into eax
movzx ax, al ; extends al into ax
movzx ebx, ax ; extends ax into ebx

对于有符号数,用mov无法实现扩展。

但8086提供了一些指令扩展有符号数。

指令 翻译 描述
CBW Convert Byte to Word 将AL扩展为AX
CWD Convert Word to Double word 将DX扩展为DX:AX

记住8086没有32位寄存器。所以只能将DX:AX两个16-bit寄存器看成一个32-bit寄存器。

80386增加了一些指令扩展有符号数。

指令 翻译 描述
CWDE Convert Word to Double word Extended 将AX扩展为EAX
CDQ Convert Double word to Quad word 将EAX扩展为EDX:EAX
MOVSX (像MOVZX那样工作,但是针对有符号数)
应用到C语言(Application to C programming)

例1

unsigned char uchar = 0xFF;
signed char schar = 0xFF;
int a = (int ) uchar; // a = 255 (0x0000FF)
int b = (int ) schar ; // b = −1 (0xFFFFFFFF)

ANSI C并没有规定char类型是有符号的还是无符号的,故交由不同编译器去决定这一点。这就是为什么在此例中显式地定义了char类型有无符号。

在此例中,第三行的数据运用了无符号数的扩展规则扩展(movzx),第四行用有符号数的规则扩展(movsx)。

例2

char ch;
while( (ch = fgetc(fp )) != EOF ) {
	// do something with ch
}

这段代码暗含一个常见bug。

fgetc的函数原型(prototype)是:

int fgetc( FILE * );

可是fgetc读的明明是字符,为什么返回int呢?
原因是通常fgetc确实返回字符,但当读到文件结尾时,它就会返回一个EOF宏(通常定义为-1)。所以fgetc要么返回一个由char扩展而来的int(在16进制下为000000xx),要么返回EOF(在16进制下为FFFFFFFF)。

fgetc返回int,但在例2中却用char类型存储这个int。C语言在这时会将int的高位截断。问题在于000000FFFFFFFFFF都会被截断为FF,while循环的判断如何区分这两者呢。

关键在于,char类型是否有符号。
在while循环的判断中,ch与EOF作对比。因为EOF是int类型值,所以为了与EOF比较,ch会被扩展为int(这样两者才拥有相同长度)。

若char是无符号的,FF就会被扩展为000000FF,与EOF(即FFFFFFFF)进行比较,发现不相等。于是while循环永不终止!

若char是有符号的,FF就会被扩展为FFFFFFFF。如此一来,循环可以终止,但新的问题是,FF也可能是由某个不是EOF的字符截断而来,这样就无法保证只有读到文件结尾时才终止循环。

所以应该将ch定义为char类型而非int类型,才不会出现上述问题。

2.1.3 补码运算(Two’s complement arithmetic)

加减运算

add指令用于加法,sub指令用于减法。
这两个指令的执行将会影响标志寄存器中的溢出位(overflow)和进位(carry flag)的值。
当计算结果太长时,溢出位将被置为1;当做加法时最高位(msb)有进位或做减法时最高位有借位时,进位将被置为1。
所以这两个标志位可以用于检测无符号运算时是否有溢出。
用补码运算时,加法与减法规则与无符号运算完全一致。
所以addsub可以用于有符号和无符号的整数运算。

乘法运算

mul用于计算有符号整数乘法;
imul用于计算无符号整数乘法。

FF这个1字节的数据,在有符号数中是255,无符号数中是-1。若两个FF相乘,有符号数情况下将得到255×255=65025(八进制为FE01);无符号数情况下将得到-1×-1=1(八进制为0001)。所以需要不同的指令处理这两种情况。

乘法指令有很多形式,最古老的形式为:
mul source

source可以是寄存器或内存,但不能为立即数。

imul指令的格式与mul相似,还增加了2操作数和3操作数格式。

imul dest, source1
imul dest, source1, source2

下表是可能的组合:

dest source1 source2 Action
reg/mem8 AX = AL×source1
reg/mem16 DX:AX = AX×source1
reg/mem32 EDX:EAX = EAX×source1
reg16 reg/mem16 dest ×= source1
reg32 reg/mem32 dest ×= source1
reg16 immed8 dest ×= immed8
reg32 immed8 dest ×= immed8
reg16 immed16 dest ×= immed16
reg32 immed32 dest ×= immed32
reg16 reg/mem16 immed8 dest = source1×source2
reg32 reg/mem32 immed8 dest = source1×source2
reg16 reg/mem16 immed16 dest = source1×source2
reg32 reg/mem32 immed32 dest = source1×source2
除法运算

有符号数:div
无符号数:idiv

格式为:div source

idiv的格式与div相同。

若商太大了,寄存器无法存放,或者除数为0,程序将中断并终止。
一个常见的错误是做除法前忘记初始化DX或EDX。

取倒数运算

neg operand,operand可以是1B、2B、4B寄存器者内存。

2.1.5 扩充精度运算(Extended precision arithmetic)

长度大于4B的数据的加减法需借助指令:adcsbb
adc计算原理:

opreand1 = operand1 + carry flag + operand2

sbb

operand1 = operand1 - carry flag - operand2

计算方法:
若参与计算的8B整型值分别存储在EDX:EAX和EBX:ECX中

add eax, ecx ; add lower 32-bits
adc edx, ebx ; add upper 32-bits and carry from previous sum

将计算出其和并存储在EDX:EAX中;

sub eax, ecx ; subtract lower 32-bits
sbb edx, ebx ; subtract upper 32-bits and borrow

将计算EBX:ECX - EDX:EAX并存储在EDX:EAX中.

对于更长的数字,可以使用循环,在循环中使用adc指令来计算和差。
可以在循环开始前使用clc(CLear Carry)指令来清空进位。当进位为0时,addadc指令的效果是相同的。
同样的思想可以运用于减法。

2.2 控制结构(Control Structures)

2.2.1 比较(Comparisons)

cmp A B执行A-B,但不会保存结果,只根据结果修改标志寄存器内容。

别忘了其它指令也能改变标志寄存器信息。

2.2.2 分支指令

分支指令类型 描述
JMP [code label] 无条件跳转
JZ branches only if ZF is set
JNZ branches only if ZF is unset
JO branches only if OF is set
JNO branches only if OF is unset
JS branches only if SF is set
JNS branches only if SF is unset
JC branches only if CF is set
JNC branches only if CF is unset
JP branches only if PF is set
JNP branches only if PF is unset

PF(parity flag)为奇偶标志位。

short jmp \[code label\]:只可以向前/后跳转128B,因为它只用1个有符号的字节记录跳转位移。

near \[分支类型指令\] \[code label\]:就是默认的跳转类型,可以跳转至内存中的任何位置。

far \[分支类型指令\] \[code label\]:可以跨端跳转(几乎用不着)

同时还有一些更易读的指令:

Signed Unsigned
JE branches if vleft = vright JE branches if vleft = vright
JNE branches if vleft != vright JNE branches if vleft != vright
JL, JNGE branches if vleft < vright JB, JNAE branches if vleft < vright
JLE, JNG branches if vleft ≤ vright JBE, JNA branches if vleft ≤ vright
JG, JNLE branches if vleft > vright JA, JNBE branches if vleft > vright
JGE, JNL branches if vleft ≥ vright JAE, JNB branches if vleft ≥ vright

2.2.3 循环指令(The loop instructions)

80x86提供了很多实现类似for循环的指令,它们都接收一个代码标签作为唯一的操作数。

后两条指令擅长顺序搜索循环,例如:

sum = 0;
for( i=10; i >0; i−− )
	sum += i;

可以转化为

	mov eax, 0 ; eax is sum
	mov ecx, 10 ; ecx is i
loop_start:
	add eax, ecx
	loop loop_start

评论