前面的章节介绍了汇编中的一些指令,而很多指令都需要对操作数进行处理,这些操作数可以存放在寄存器中,也可以存放在内存中,它们可以用来表示很多不同的数据类型,比如整数,浮点数,BCD码等,...

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

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

    前面的章节介绍了汇编中的一些指令,而很多指令都需要对操作数进行处理,这些操作数可以存放在寄存器中,也可以存放在内存中,它们可以用来表示很多不同的数据类型,比如整数,浮点数,BCD码等,不同的类型之间还可以通过特殊的指令进行转换,下面就对汇编中不同的数据类型进行介绍,并通过例子来演示这些数据的实际用法。

Numeric Data Types 数字数据类型:

    汇编中经常要处理数字,IA-32平台就包含了多种不同的数字类型,其中基本的数字类型如下:
  • Unsigned integers
    无符号整数
  • Signed integers
    有符号整数
  • Binary-coded decimal
    二进码十进数或二-十进制代码 (简称为BCD码)
  • Packed binary-coded decimal
    压缩的BCD码
  • Single-precision floating-point
    单精度浮点数
  • Double-precision floating-point
    双精度浮点数
  • Double-extended floating-point
    双精度扩展浮点数
    在前面的"IA-32平台(三) 硬件介绍结束篇及各种处理器平台"章节中,提到过奔腾处理器引入了一种SIMD即单指令多数据模式技术,在SIMD中添加了如下几种高级的数字类型:
  • 64-bit packed integers
    64位压缩整数
  • 128-bit packed integers
    128位压缩整数
  • 128-bit packed single-precision floating-point
    128位压缩单精度浮点数
  • 128-bit packed double-precision floating-point
    128位压缩双精度浮点数
    上面显示的数字类型确实比较多,但是在汇编中使用它们并不难,下面就针对每种类型进行介绍,并通过例子来说明如何在汇编中使用它们。

Integers 整数:

    汇编程序中使用的最基本的数字类型就是整数了,它们可以表示一个大范围的整数值,下面就介绍不同的整数类型以及处理器是如何处理这些整数的。

Standard integer sizes 标准的整数大小:

    一个整数可以有不同的尺寸大小,不同尺寸对应的字节数不同,IA-32平台一般支持四种整数大小:
  • Byte: 8 bits
    字节大小即8位二进制的大小
  • Word: 16 bits
    字大小对应两个字节即16位二进制的大小
  • Doubleword: 32 bits
    双字大小对应两个字即32位二进制的大小
  • Quadword: 64 bits
    四字大小,顾名思义对应四个字即64位二进制的大小
    这里需要注意的是,当整数的尺寸大于一个字节时,它在内存中是以little-endian即小字节序进行存储的,也就是整数的最低字节是最靠近内存的绝对地址0的,其他的字节则紧挨着依次存放在向后递增的地址空间上。但是当整数的值被加载到寄存器中后,它在寄存器中又是以相反的big-endian即大字节序进行存储的,这一点有时就会让人感到困惑,如下图所示:



图1

    上图显示了内存低地址的数据对应寄存器里的高地址(在寄存器中应该也有类似地址总线的东东),不过,这种内存与寄存器间的小字节序到大字节序的转换工作是在处理器内部完成的,所以也就不需要担心寄存器里的大字节序。一般情况下,在做汇编的开发和调试时,最需要留意的还是内存里面的小字节序,如下面的例子:

(gdb) x/x &data
0x80490bc <data>: 0x00000225
(gdb) x/4b &data
0x80490bc <data>: 0x25 0x02 0x00 0x00
(gdb) print/x $eax
$1 = 0x225
(gdb)

    上面的gdb调试中,先用x/x命令将data内存位置里的整数值以十六进制的形式输出显示出来,可以看到data里的值是0x225(十六进制格式),接着通过x/4b命令将data里的数据以小字节序,一个字节一个字节的输出显示出来,0x25即data整数的最低字节存放在最左侧即data内存的最低位置,0x02高字节存放在随后的内存地址加一的位置,如果不注意的话就容易误读成0x2502,而其真实的值应该是0x225,所以小字节序在调试内存数据时,是需要特别加以留意的地方。

    最后通过print/x命令将EAX里的值显示出来,这里的EAX在之前已经使用mov指令被赋予data内存里的值了,从输出结果可以看出来,尽管EAX里的数据是以大字节序存放的,但是在实际使用时,内部会自动进行转换,所以并不影响正常使用,平时汇编开发调试时也无需关心寄存器里的大字节序的存储情况。

Unsigned integers 无符号整数:

    无符号整数是一种"所见即所得"的数据类型,组成整数的字节里的值直接就代表了该整数的值,也就是字节里的所有二进制位都用于表示数值,所以它的值只会大于等于0,不会是负数,这也就是"无符号“的含义。

    无符号整数根据字节数的不同,有如下4种尺寸大小:

Bits 
二进制位
Integer Values 
对应的无符号整数范围
8位 0 through 255 
0到255
16位 0 through 65,535 
0到65,535
32位 0 through 4,294,967,295 
0到4,294,967,295
64位 0 through 18,446,744,073,709,551,615 
0到18,446,744,073,709,551,615

    一个8位无符号整数刚好占用一个字节的大小,例如某个字节里的二进制位为:11101010即十六进制0xEA,所表示的无符号整数值为234 。

    16位无符号整数由两个字节即一个字组成,下图演示了16位无符号整数在寄存器中的存储情况:



图2

    上图中两个字节0x06和0x88拼在一起就可以表示无符号整数1672  (图中寄存器里是大字节序,所以可以直接拼在一起,无需调整两字节的顺序)。

    32位无符号整数由4个字节即双字组成,在IA-32平台中,这种32位的双字大小的无符号整数是最常使用的数据格式之一,下图演示了寄存器中双字的存储情况:
 


图3

    图中,每个字节代表一个十六进制对如0x86,每4个二进制位对应一个十六进制值,如10000110中的前4位1000对应0x8,后4位0110对应0x6,合在一起就得到无符号整数的最高字节0x86,所以上图中的32位数就有8个十六进制值即0x866EB213,对应的十进制格式为2255401491 (同样的,本例中寄存器里是大字节序)。

    64位无符号整数由8个字节即4个字组成,下图为64位的存储情况:
 


图4

Signed integers 有符号整数:

    无符号整数简单易用,"所见即所得",但缺点是它无法表示负数,要解决这个问题,就需要引入一种可以应用到处理器上的表示负数的方法,计算机中有三种常见的描述负数的方法:
  • Signed magnitude
    符号-数值码,简称原码
  • One’s complement
    二进制反码,简称反码
  • Two’s complement
    二进制补码,简称补码
    这三种方式所表示的有符号整数具有和无符号整数一样的位尺寸大小 (如可以是字节大小,字大小,双字或四字大小) ,只不过有符号整数的字节里的二进制位所表示的十进制值不同而已。IA-32平台使用的是two's complement即补码的方式来表示有符号整数的,尽管IA-32平台没使用另外两种方式,但最好都了解一下,所以下面就分别对它们进行介绍。

Signed magnitude 符号-数值码,即原码:

    原码将有符号数拆分为两个部分:一个符号位,其余的为数值位。字节里的最高位即最左侧的位是符号位,用于表示该数是正数还是负数,正数的符号位为0,负数的符号位为1,字节里剩下的位用于表示该数的值,如下图所示:



图5

    上图中,最高位即最左侧的位是符号位,图中为1,所以是个负数,剩下的二进制位 0011011 对应的值27就是数值部分,所以图中以原码表示方法所表示的就是十进制数-27 。

    原码的表示方法有个问题,就是0有两种二进制表示方式:00000000 (对应十进制值 +0) 和 10000000 (对应十进制值 -0) ,这会让数学处理过程变得复杂。另外原码的表示方式会让数学加减运算变得复杂,例如,将 00000001 (对应十进制值1) 和 10000001 (对应十进制值-1) 使用add指令相加,得到的结果会是 10000010 (对应十进制值-2) ,这样1和-1在add加法指令下得出的结果为-2,显然不对,这表明如果用原码来表示有符号数,那么处理器就需要两套加减指令集,一套用于无符号数,一套用于有符号数。

One's complement 二进制反码,简称反码:

    二进制反码的方式就是直接将二进制位进行反转来表示负数,即将所有位1变为位0,所有位0变为位1,例如:00000001 的反码就是 11111110 。这种反码的方式和原码一样,在执行数学运算操作时会遇到一些问题,比如,0也有两种表示方法:00000000 和 11111111 ,反码也会让处理器的数学运算变得复杂。

Two's complement 二进制补码,简称补码:

    补码的方式通过一个简单的数学技巧,有效的解决了原码和反码中出现的数学问题。补码通过在反码的基础上简单的加个一来表示负数。

    例如,要得到负数 -1 的二进制补码形式,你可以通过以下两个步骤来完成:
  • 先得到正数 1 即 00000001 的反码:11111110
  • 对上面的反码加一 :11111111
    所以二进制 11111111 在补码的角度来看,它就表示 -1 。同理 -2 就是 11111110 ,-3 就是 11111101 ,这样我们就可以得到单个字节大小的有符号整数的有效负数范围是 11111111 (即-1) 一直递减到 10000000 (即-128) ,其他多字节的有符号整数的有效值范围也可以同理得出。

    这种方式看起来有点怪,但它却有效的解决了有符号数在加减运算时的所有问题,例如,将 00000001 (+1) 和 11111111 (-1) 相加,得到的结果为 00000000 ,同时会产生一个进位标志,该进位标志在进行有符号数的数学运算时会被忽略掉,所以结果就是0,这样加法和减法指令就可以同时用于有符号数和无符号数的数学运算,通过你的汇编程序逻辑和EFLAGS标志寄存器里的各种标志来确定你用的是有符号数还是无符号数。

    有符号整数虽然和无符号数在二进制位尺寸上没有什么不同,但是它有一半的二进制排列组合情况是用于表示负数的,例如前面提到的单字节大小的有符号整数中,11111111 到 10000000 用于表示 -1 到 -128 ,而 00000001 到 01111111 用于表示 +1 到 +127 ,最后还剩下一个 00000000 用于表示 0 ,负数占掉一半,且单字节有符号整数的最大值127是单字节无符号整数的最大值255的一半,下表显示了各种尺寸的有符号整数的有效值范围:

Bits 
二进制位
Minimum and Maximum Signed Values
对应的有符号整数的有效值范围
8位 -128 to 127 
-128 到 127
16位 -32,768 to 32,767 
-32,768 到 32,767
32位 -2,147,483,648 to 2,147,483,647
-2,147,483,648 到 2,147,483,647
64位 -9,223,372,036,854,775,808 to  9,223,372,036,854,775,807
-9,223,372,036,854,775,808 到9,223,372,036,854,775,807

Using signed integers 有符号整数的使用:

    从纯二进制的角度,要完全判断出寄存器和内存里的值是无符号整数还是有符号整数是件很困难的事,因为这些二进制值既可以当成无符号数也可以当成有符号数,例如:11111111 如果你把它看成有符号数,那么它就是 -1 ,如果你把它看成无符号数,那么它就是 255 ,所以这取决于你怎么用它,GNU调试器可以对此做一些简单的判断,不过只能作为参考,不能以此为准,下面的 inttest.s 程序是有符号整数使用的简单例子:

# inttest.s - An example of using signed integers
.section .data
data:
    .int -45
.section .text
.globl _start
_start:
    nop
    movl $-345, %ecx
    movw $0xffb1, %dx
    movl data, %ebx
    movl $1, %eax
    int $0x80

    上面的例子演示了将有符号数加载到寄存器中的三种方式,其中,前两种方式使用十进制和十六进制的立即数形式将负数加载到寄存器中:

movl $-345, %ecx
movw $0xffb1, %dx

    第三种方式是使用data标签名将该标签所在内存位置里的有符号整数给加载到寄存器里。

    然后对程序进行汇编,调试:

$ as -gstabs -o inttest.o inttest.s
$ ld -o inttest inttest.o
$ gdb -q inttest

Reading symbols from /root/asm_example/inttest/inttest...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file inttest.s, line 9.
(gdb) r
Starting program: /root/asm_example/inttest/inttest

Breakpoint 1, _start () at inttest.s:9
9        movl $-345, %ecx

(gdb) s
10        movw $0xffb1, %dx
(gdb) s
11        movl data, %ebx
(gdb) s
12        movl $1, %eax
(gdb) info reg
eax            0x0    0
ecx            0xfffffea7    -345
edx            0xffb1    65457
ebx            0xffffffd3    -45

    从上面的输出可以看出来,ECX和EBX两个寄存器的info输出情况和我们所期望的一样,将程序中设置的负数显示了出来,但是EDX在gdb中显示的提示是65457,这是因为 movw $0xffb1, %dx 这条指令只设置了EDX寄存器中的低16位的值为0xffb1,但是EDX高位的值还是0,EDX里的完整值为0x0000ffb1,gdb的info命令显示的是按4字节的有符号整数进行显示的,对于2字节的有符号整数而言,0xffb1是-79,但是对于4字节的有符号整数而言,0x0000ffb1就是正数65457,所以info显示的EDX值为65457

    不管是-79也好,还是65457也好,都只是一个概念,从你的角度看来它是负数,从gdb看来它是正数,但只要EDX里的二进制值是0xffb1就没错,gdb的提示信息并不会影响你的程序逻辑。

Extending integers 对整数的大小进行扩展:

    有时候我们会对某个整数的大小进行扩展,比如将整数由字节大小扩展为字大小,或者由字大小扩展为双字大小,这看起来挺简单,但具体操作时还是有需要注意的地方,下面就分别介绍无符号整数和有符号整数的大小扩展方法。

Extending unsigned integers 对无符号整数的大小扩展:

    在对无符号整数进行大小扩展时,不能简单的拷贝就完事,还需要确保将扩展后的高位设置为0 ,例如下面的代码:

movw %ax, %bx

    这条指令只是简单的将AX里的值拷贝到BX中,但这并不能保证EBX里的高位字节一定是0,要确保EBX的高位字节为0,就需要如下两条指令:

movl $0, %ebx
movw %ax, %ebx

    先通过movl用0填充EBX,然后再将AX里的值拷贝到EBX中,这样AX里的2字节大小的无符号整数,才能扩展为4个字节,同时只有EBX高位为0了,才能确保2字节和扩展后的4字节里的值是相等的,例如AX里的值为0xa1b2 ,经过上面两条指令后,才能变为 0x0000a1b2 ,这样既扩展了大小,又保持了值不变。

    可能你会觉得使用两条指令太麻烦了,而且有时候容易忘记清零,不过庆幸的是,英特尔提供了一个MOVZX指令,该指令可以在扩展无符号整数大小时,帮你自动完成清零的工作。

    该指令的格式如下:

movzx source, destination

    source源操作数可以是8位或16位的寄存器,也可以是8位或16位的内存位置,destination目标操作数则必须是16位或32位的寄存器。

    下面的movzxtest.s程序演示了该指令的用法:

# movzxtest.s - An example of the MOVZX instruction
.section .text
.globl _start
_start:
    nop
    movl $279, %ecx
    movzx %cl, %ebx
    movl $1, %eax
    int $0x80

    对movzxtest.s进行汇编调试:

$ as -gstabs -o movzxtest.o movzxtest.s
$ ld -o movzxtest movzxtest.o
$ gdb -q movzxtest

Reading symbols from /root/asm_example/movzx_movsx/movzxtest...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048055: file movzxtest.s, line 6.
(gdb) r
Starting program: /root/asm_example/movzx_movsx/movzxtest

Breakpoint 1, _start () at movzxtest.s:6
6        movl $279, %ecx

(gdb) s
7        movzx %cl, %ebx
(gdb) s
8        movl $1, %eax
(gdb) print $ecx
$1 = 279
(gdb) print $ebx
$2 = 23
(gdb) print/x $ecx
$3 = 0x117
(gdb) print/x $ebx
$4 = 0x17
(gdb)

    代码中先将279赋值给ECX寄存器,然后再将CL即ECX中最低字节的值通过movzx扩展到EBX里,由于279(十六进制为0x117)超过了一个字节的最大值255,所以CL里存放的值为23(十六进制为0x17),CL经过扩展以后,EBX里的值也是23也就是0x17 (在movzx扩展时,EBX一开始会被自动清零,所以EBX里的值可以确保和CL里的一致),通过上面gdb的调试输出结果也可以验证这一点。

Extending signed integers 有符号整数的大小扩展:

    有符号整数的大小扩展和无符号整数的扩展有所不同,因为如果按照无符号的方法将高位填充为零,那么就很有可能会改变有符号数的正负数性质,例如,-1 (11111111) 如果使用movzx指令扩展为2个字节的话,就会变为 0000000011111111 (+127) ,这样 -1 扩展后就变为了 +127 ,显然不对,所以有符号整数在进行扩展时,如果是正数,高位应填充0,如果是负数,高位应填充1,才能确保扩展后的值不变。

    英特尔提供了MOVSX指令来完成有符号整数的大小扩展,该指令既可以扩展大小,又可以维持有符号整数的符号性质。

    下面的movsxtest.s程序演示了该指令的用法:

# movsxtest.s - An example of the MOVSX instruction
.section .text
.globl _start
_start:
    nop
    movw $-79, %cx
    movl $0, %ebx
    movw %cx, %bx
    movsx %cx, %eax
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    对movsxtest.s进行汇编,调试:

$ as -gstabs -o movsxtest.o movsxtest.s
$ ld -o movsxtest movsxtest.o
$ gdb -q movsxtest

Reading symbols from /root/asm_example/movzx_movsx/movsxtest...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048055: file movsxtest.s, line 6.
(gdb) r
Starting program: /root/asm_example/movzx_movsx/movsxtest

Breakpoint 1, _start () at movsxtest.s:6
6        movw $-79, %cx

(gdb) s
7        movl $0, %ebx
(gdb) s
8        movw %cx, %bx
(gdb) s
9        movsx %cx, %eax
(gdb) s
10        movl $1, %eax
(gdb) info reg
eax            0xffffffb1    -79
ecx            0xffb1    65457
edx            0x0    0
ebx            0xffb1    65457

    代码中先将-79赋值给CX ,因为只设置了ECX的低16位,ECX高16位还是0,所以info显示的ECX值为65457,接着通过movl $0, %ebx和movw %cx, %bx两条指令按照无符号扩展的做法,将CX里的值扩展到EBX中,这样EBX里的值就是0x0000ffb1 (65457) ,只扩展了大小,没有保留有符号值的符号属性 ,最后使用movsx指令将CX里的值按照有符号的方式扩展到EAX中,从info的输出结果可以看出,EAX里的值 0xffffffb1 (-79) 既扩展了CX里值的大小(由2个字节变为4个字节),又保持了值的符号属性(movsx通过将高位全部填充为1来保持负数的符号属性不变)。

    下面我们通过movsxtest2.s程序来看下movsx指令在遇到正整数时,是如何进行扩展的:

# movsxtest2.s - Another example using the MOVSX instruction
.section .text
.globl _start
_start:
    nop
    movw $79, %cx
    xor %ebx, %ebx
    movw %cx, %bx
    movsx %cx, %eax
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    对movsxtest2.s进行汇编,调试如下:

$ as -gstabs -o movsxtest2.o movsxtest2.s
$ ld -o movsxtest2 movsxtest2.o
$ gdb -q movsxtest2

Reading symbols from /root/asm_example/movzx_movsx/movsxtest2...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048055: file movsxtest2.s, line 6.
(gdb) r
Starting program: /root/asm_example/movzx_movsx/movsxtest2

Breakpoint 1, _start () at movsxtest2.s:6
6        movw $79, %cx

(gdb) s
7        xor %ebx, %ebx
(gdb) s
8        movw %cx, %bx
(gdb) s
9        movsx %cx, %eax
(gdb) s
10        movl $1, %eax
(gdb) info reg
eax            0x4f    79
ecx            0x4f    79
edx            0x0    0
ebx            0x4f    79

    上面的代码中先将正整数79赋值给CX ,然后还是按照无符号的方式将CX值扩展到EBX中,这里可以看到另一种清零的方法,通过xor异或指令也可以将EBX寄存器清零(xor指令进行位运算时,是按照两操作数相同位清零,不同位设置为1的方式,例如:1011 和 1101 进行xor异或运算后结果就是 0110 ,这里xor两个操作数都是EBX,位都相同,所以就将EBX清零了),将EBX清零后,再用movw %cx , %bx指令将CX的值扩展到EBX寄存器中,扩展后EBX里的值为79 ,说明对于正整数,是可以使用无符号的方式进行扩展的。最后通过movsx指令将CX的值扩展到EAX中,info输出结果显示EAX的值也是79,说明当movsx的源操作数为正整数时,它会和无符号扩展一样直接将高位清零。

    所以对于无符号整数的扩展,最好使用movzx指令,对于有符号整数的扩展,最好使用movsx指令,另外对于正整数的扩展,使用movzx和movsx产生的效果是一样的。

    限于篇幅,本篇就到这里,未完待续,转载请注明来源:www.zengl.com

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

下一篇: 汇编数据处理 (二)

上一篇: 汇编流程控制 (三) 流程控制结束篇

相关文章

全书结束篇 使用IA-32平台提供的高级功能 (四) SSE2、SSE3相关指令

汇编里使用Linux系统调用 (三) 系统调用结束篇

汇编流程控制 (二) 条件分支及汇编循环

汇编数据处理 (三) 浮点数

汇编函数的定义和使用 (三) 汇编函数结束篇

优化汇编指令 (三)