Linux汇编之整数

汇编 2015年03月15日 ,

Linux汇编整数

汇编语言中使用的最基本的数字形式是整数。它可以表示很大范围的值。本文中枫竹梦介绍Linux汇编语言(ASM)中的可以使用的不同整数类型,并且介绍处理器如何处理各种不同类型的整数值。

标准整数长度

IA-32平台支持4种不同的整数长度:

  • 字节(Byte):8位
  • 字(Word):16位
  • 双字(Doubleword):32位
  • 四字(Quadword):64位

注意:存储在内存中的超过1个字节的整数被存储为小端(Little-endian)格式。低位字节存储在低位内存位置,其余字节顺序存放。但是,把整数传递给寄存器时,值是按照大端(Big-endian)格式存储在寄存器中的。如:

内存与寄存器中存储顺序不同

这一转换在处理器中悄悄进行,所以不必担心转换问题。但是在调试程序时要注意这个问题。

无符号整数

可以将无符号整数理解为所见即所得的数据类型。组成整数的字节的值直接表示整数值。

不同长度的整数表示不同的范围,如下:

整数值
8 0~255
16 0~65535
32 0~4294967295
64 0~18446744073709551615

8位整数值包含在单一字节之内。字节中包含的二进制值就是实际的整数值。如,二进制值为11101010的字节(可以表示为十六进制值0xEA)的无符号整数值是234

16位无符号号整数值包含在两个连续的字节中,它们组合在一起构成一个字。如显示在寄存器中的字值为00000110 10001000,十六进制为0x0688,对应十进制数为1672

32位无符号数和64位无符号数分别占用4字节和8字节。它们的组合方式与16位无符号数类似,时刻注意内存中数的存储顺序与寄存器中的不同。

带符号整数

虽然使用无符号数很方便且很容易,但是它不能表示负数。有3种方法在处理器上表示负数:

  • 带符号数值
  • 反码(One's complement)
  • 补码(Two's complement)

这3种方法都使用与无符号整数相同的位长度,但是在位中表示十进制值的方式是不同的。IA-32中使用补码的方式表示带符号整数。

带符号数值

带符号数值的表示方法将整数分为两部分:符号位和数值位。字节的最大有效位(最左侧的一位)用于表示值的符号。规定正数的最大有效位为0,而负数的最大有效位为1。其余位使用一般的二进制值表示数字的数值。如16位数字表示:10000001表示-1,00000001表示1。

带符号数值表示的一个问题是有两种表示0值的方式:00000000(十进制+0)和10000000(十进制-0)。有些时候这会使问题复杂会。另外一个问题是数值运算不复杂的,不能按照无符号数字的方式进行。如加法00000001(十进制1)和10000001(十进制-1)相加得到10000010(十进制-2),这不是正确的答案。这要求处理器为带符号数和无符号数提供不同的数学运算指令。

反码

反码采用无符号整数的相反的代码生成相应的负值。正数采用与无符号方式相同的二进制表示;负数按位求反。求反是把所有为0的位改变为1,所有为1的位改变为0。如,-1按无符号1的二进制位00000001取反得到111111100000000011111111都表示0,同样给运算增加了复杂度。

补码

补码采用了简单的数学技巧,解决了带符号数值和反码方法的数学运算问题。对于负整数,值的反码加上1就是它的补码。如求-1的补码。

  • 得到00000001的反码,结果是11111110
  • 反码加上1,结果是11111111

对于多字节整数长度,相同的规则跨字节适用。

虽然这样做不容易理解,可是它解决了带符号数的加法和减法的所有问题。如值00000001(+1)和11111111(-1)相加得到00000000,同时带有进位值,得到0值采用相同的硬件可以直接进行无符号数和带符号数的加法和减法运算。

对于相同位长度的数,被码格式表示的值的数量和无符号整数对应的值的数量是相同的,带符号数正值的表示范围没有无符号数大,带符号数补码表示范围如下:

整数值
8 -128~127
16 -32768~32767
32 -2147483648~2147483647
64 -9223372036854775808~9223372036854775807

使用带符号整数

内存或者寄存器中表示的带符号整数经常是难以识别的,除非知道期望的是什么。有时候GNU调试器能够有所帮助,有时也会适得其反,如下程序:

# int.s - By furzoom @ Mar 12, 2015
.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

如上程序演示了3种将带符号数传送给寄存器的方式。前2中使用立即数,后1种将标签引用内存位置传送给寄存器。将程序汇编,并调试运行。当将所有的数据都传送给寄存器时,查看寄存器的值,下面截取相关寄存器的一部分:

(gdb) info reg
(gdb) info registers
eax            0x0      0
ecx            0xfffffea7      -345
edx            0xffb1      65457
ebx            0xffffffd3      -45

调试器假设EBXECX寄存器中包含带符号整数,并且使用期望的数据类型显示答案。而调试器将整个EDX寄存器看作是带符号整数数据值显示。寄存器中的值是正确的,只是不是以期望的形式显示出来。

扩展整数

有时需要将整数值传送到长度大一些的内存位置,如把字传送给双字,虽然是很小的事情,但有时并不简单。

扩展无符号整数

把无符号整数值转换为倍数更大的值时,必须确保所有的高位部分都被设置为零。不应该把一个值复制给另一个值。如:

movw %ax, %bx

这不能保证EBX寄存器的高位部分包含零。应该按照如下的方式进行操作:

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

为了帮助程序员应对这种情况,Intel提供了MOVZX指令。用于把长度小的无符号整数值(寄存器或者内存)传送给长度大的无符号整数值(只能在寄存器中)。格式如下:

movzx source, destination

其中source可以是8位或者16位内存位置,destination可以是16位或者32位寄存器。如下一个完整的示例程序演示这个命令:

# movzx.s - By furzoom @ Mar 12, 2015
.section .text
.globl _start
_start:
nop
movl $279, %ecx
movzx %cl, %ebx
movl $1, %eax
int $0x80

该程序简单地把一个大的值放到ECX寄存器中,然后使用MOVZX指令把低8位复制到EBX寄存器。因为存放在ECX寄存器中的值使用长度为字的无符号整数表示它,所以CL中的值只表示完整值的一部分。调试运行如下:

[mn@furzoom asm]$ as -gstabs -o movzx.o movzx.s
[mn@furzoom asm]$ ld -o movzx movzx.o
[mn@furzoom asm]$ gdb -q movzx
Reading symbols from /home/mn/Desktop/Documents/asm/movzx...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048055: file movzx.s, line 6.
(gdb) run
Starting program: /home/mn/Desktop/Documents/asm/movzx

Breakpoint 1, _start () at movzx.s:6
6         movl $279, %ecx
(gdb) step
7         movzx %cl, %ebx
(gdb) step
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)

通过输出EBXECX寄存器的十进制值,发现无符号整数值并没有正确地复制。输出十六进制值发现,MOVZX指令中传送了ECX寄存器的低位字节,而用0填充EBX中的剩余字节。

扩展带符号整数

扩展带符号整数值和扩展无符号整数是不同。使用零填充高位会改变负数的数据值。如把值-1(11111111)传送给双字会生成0000000011111111,带符号数表示127,而不是-1。扩展带符号数的负值时,需要在高位填充1,得到1111111111111111,才是带符号数的-1,才是正确的值。

同样Intel提供了MOVSX指令,它允许扩展带符号整数并且保留符号。它指令格式与MOVZX类似,但其假设要传送的操作数中带符号数。如下例所示:

# movsx.s - By furzoom @ Mar 13, 2015
.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

该程序在CX寄存器中定义一个负值。然后试图把这个值复制到EBX寄存器中,程序首先用零填充EBX寄存器,然后使用MOV指令。下一步,使用MOVSX指令把CX寄存器的值传送给EAX寄存器。调试运行如下:

(gdb) info registers
eax            0xffffffb1       -79
ecx            0xffb1   65457
edx            0x0      0
ebx            0xffb1   65457

单步运行直到MOVSX指令之后,ECX寄存器的值为0x0000FFB1,低16包含的值为0xFFB1,它是带符号整数格式的-79。当CX寄存器被传送给EBX寄存器时,EBX寄存器包含的值是0x0000FFB1,它是带符号整数格式65457。

修改程序如下:

# movsx2.s - By furzoom @ Mar 13, 2015
.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

调试运行结果如下:

(gdb) info registers
eax            0x4f     79
ecx            0x4f     79
edx            0x0      0
ebx            0x4f     79

结果如期望的一样,对于带符号的正数,其结果也是正确的。

在GNU汇编器中字义整数

也可以在数据段中使用命令定义带符号的整数值。使用.int、.short、.long命令定义带符号整数值,它们都定义双字带符号整数值。也可以使用.quad命令定义四字的带符号整数值。如下程序使用.quad定义四字带符号整数值演示。

# quad.s - By furzoom @ Mar 13, 2015
.section .data
data1:
.int 1, -1, 463345, -333252322, 0
data2:
.quad 1, -1, 463345, -333252322, 0
.section .text
.globl _start
_start:
nop
movl $1, %eax
movl $0, %ebx
int $0x80

该程序分别定义了2组包含5个相同值的双字和四字数据,调试运行如下:

[mn@furzoom asm]$ as -gstabs -o quad.o quad.s
[mn@furzoom asm]$ ld -o quad quad.o
[mn@furzoom asm]$ gdb -q quad
Reading symbols from /home/mn/Desktop/Documents/asm/quad...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file quad.s, line 11.
(gdb) run
Starting program: /home/mn/Desktop/Documents/asm/quad

Breakpoint 1, _start () at quad.s:11
11        movl $1, %eax
(gdb) x/5d &data1
0x8049084 <data1>:      1       -1      463345  -333252322
0x8049094 <data1+16>:   0
(gdb) x/5d &data2
0x8049098 <data2>:      1       0       -1      -1
0x80490a8 <data2+16>:   463345
(gdb)

查看data1data2数组的十进制数时,data1如期望一样,而data2输出好像不正确,是因为调试器将数值理解为双字带符号数的原因。继续查看:

(gdb) x/20xb &data1
0x8049084 <data1>:      0x01    0x00    0x00    0x00    0xff    0xff    0xff    0xff
0x804908c <data1+8>:    0xf1    0x11    0x07    0x00    0x1e    0xf9    0x22    0xec
0x8049094 <data1+16>:   0x00    0x00    0x00    0x00
(gdb) x/40xb &data2
0x8049098 <data2>:      0x01    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x80490a0 <data2+8>:    0xff    0xff    0xff    0xff    0xff    0xff    0xff    0xff
0x80490a8 <data2+16>:   0xf1    0x11    0x07    0x00    0x00    0x00    0x00    0x00
0x80490b0 <data2+24>:   0x1e    0xf9    0x22    0xec    0xff    0xff    0xff    0xff
0x80490b8 <data2+32>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
(gdb)

对于data1每个元素使用4个字节存储,data2每个元素使用8个字节存储,且都是按照小端格式存储。如果以十进制来查看64位值,使用x命令gd参数:

(gdb) x/5gd &data2
0x8049098 <data2>:      1       -1
0x80490a8 <data2+16>:   463345  -333252322
0x80490b8 <data2+32>:   0
(gdb)

(完)

如无特别说明,本站文章皆为原创,若要转载,务必请注明以下原文信息:
日志标题:《Linux汇编之整数》
日志链接:http://furzoom.com/linux-asm-integer/
博客名称:枫竹梦

【上一篇】
【下一篇】

2 篇回应 (访客:1 篇, 博主:1 篇)

  1. 爱城轨 2015-16-03

    谢谢楼主分享。爱城轨,做中国最大的轨道交通技术分享网站,http://www.metro521.com。望评论互访,谢谢。

    #-49楼

插入图片

NOTICE1:请申请gravatar头像,没有头像的评论可能不会被回复!

回到顶部