众所周知,在二进制代码中的数据是不存在类型这个概念的,我们只能知道某个寄存器或者内存上有一个多少位的数据。那么,计算机是如何来判断某个数据能进行哪些操作的呢?这就是在编译和汇编时需要完成的工作了。通过分析指令或者已知函数操作内存地址的方式,我们可以获得一些信息。
下面看几个例子。
例1
int main(){
int count = 1000;
unsigned char loop = 0;
for (; loop < count; loop++) ;
return 0;
}
其中涉及到 int 类型和 unsigned char 类型的比较。上面这段代码翻译成汇编语言是:
00000000000005fa <main>:
5fa: 55 push %rbp
5fb: 48 89 e5 mov %rsp,%rbp
5fe: c7 45 fc e8 03 00 00 movl $0x3e8,-0x4(%rbp)
605: c6 45 fb 00 movb $0x0,-0x5(%rbp)
609: 0f b6 45 fb movzbl -0x5(%rbp),%eax
60d: 39 45 fc cmp %eax,-0x4(%rbp)
610: 7e 0c jle 61e <main+0x24>
612: 0f b6 45 fb movzbl -0x5(%rbp),%eax
616: 83 c0 01 add $0x1,%eax
619: 88 45 fb mov %al,-0x5(%rbp)
61c: eb eb jmp 609 <main+0xf>
61e: b8 00 00 00 00 mov $0x0,%eax
623: 5d pop %rbp
624: c3 retq
625: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
62c: 00 00 00
62f: 90 nop
可以看出使用了 movzbl
指令先从 -0x5(%rbp)
拷贝到 %eax
,zbl
后缀表示零扩展从 byte 类型到 long 类型,所以只拷贝了低 8位。对 %eax
加1后再用 mov
把 %al
拷贝回 -0x5(%rbp)
,因为是 %al
所以只有低8位。因此,通过指令把数据限制在了低8位。
例2
我们用下面两段代码的汇编代码来看下,对于同样是32位的有符号整数和无符号整数,计算机是如何区别的。
- 代码1
#include <stdint.h>
int main(){
int32_t count = INT32_MAX;
int32_t loop = 0;
for (; loop < count; loop++) ;
return 0;
}
对应
00000000000005fa <main>:
5fa: 55 push %rbp
5fb: 48 89 e5 mov %rsp,%rbp
5fe: c7 45 fc ff ff ff 7f movl $0x7fffffff,-0x4(%rbp)
605: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
60c: 8b 45 f8 mov -0x8(%rbp),%eax
60f: 3b 45 fc cmp -0x4(%rbp),%eax
612: 7d 06 jge 61a <main+0x20>
614: 83 45 f8 01 addl $0x1,-0x8(%rbp)
618: eb f2 jmp 60c <main+0x12>
61a: b8 00 00 00 00 mov $0x0,%eax
61f: 5d pop %rbp
620: c3 retq
621: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
628: 00 00 00
62b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 代码2
#include <stdint.h>
int main(){
uint32_t count = UINT32_MAX;
uint32_t loop = 0;
for (; loop < count; loop++) ;
return 0;
}
对应
00000000000005fa <main>:
5fa: 55 push %rbp
5fb: 48 89 e5 mov %rsp,%rbp
5fe: c7 45 fc ff ff ff ff movl $0xffffffff,-0x4(%rbp)
605: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
60c: 8b 45 f8 mov -0x8(%rbp),%eax
60f: 3b 45 fc cmp -0x4(%rbp),%eax
612: 73 06 jae 61a <main+0x20>
614: 83 45 f8 01 addl $0x1,-0x8(%rbp)
618: eb f2 jmp 60c <main+0x12>
61a: b8 00 00 00 00 mov $0x0,%eax
61f: 5d pop %rbp
620: c3 retq
621: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
628: 00 00 00
62b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
从两段代码我们可以看出主要的区别就只有 612
这个地址上的内容。对于有符号数,比较时用的是 jge
,而无符号数使用 jae
。这样的原因是计算机无需数据类型,只要指令使用得正确即可执行。因此可以知道,单独从数据来看,是无法得到其类型的,我们只有通过指令操作来推断出类型。
例3
补充一个例子,和前面例2的代码2可以一起看。
#include <stdint.h>
int main(){
uint32_t count = UINT32_MAX;
int32_t loop = 0;
for (; loop < count; loop++) ;
return 0;
}
对应
00000000000005fa <main>:
5fa: 55 push %rbp
5fb: 48 89 e5 mov %rsp,%rbp
5fe: c7 45 fc ff ff ff ff movl $0xffffffff,-0x4(%rbp)
605: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
60c: 8b 45 f8 mov -0x8(%rbp),%eax
60f: 39 45 fc cmp %eax,-0x4(%rbp)
612: 76 06 jbe 61a <main+0x20>
614: 83 45 f8 01 addl $0x1,-0x8(%rbp)
618: eb f2 jmp 60c <main+0x12>
61a: b8 00 00 00 00 mov $0x0,%eax
61f: 5d pop %rbp
620: c3 retq
621: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
628: 00 00 00
62b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
分析两段汇编代码,其实含义上没有任何区别。对于例2中的代码2, cmp count, loop
时,用的是 jae
指令, loop
无符号大于等于 count
则跳转退出;对于后面这个代码, cmp loop, count
时,用的是 jbe
指令, count
无符号小于等于 loop
则跳转退出。两个意思是一样的。这说明在后面的这个例子中,执行到 loop < count
时,有符号类型变量 int32_t loop
会被强转成无符号类型。