众所周知,二进制代码中的数据没有类型的概念。我们只能知道寄存器或内存中有多少数据。那么,计算机如何判断数据可以操作什么呢?这是编译和汇编时需要完成的工作。通过分析指令或已知函数操作内存地址,我们可以获得一些信息。
以下是几个例子。
例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),?x 60d: 39 45 fc cmp ?x,-0x4(%rbp) 610: 7e 0c jle 61e <main 0x24> 612: 0f b6 45 fb movzbl -0x5(%rbp),?x 616: 83 c0 01 add $0x1,?x 619: 88 45 fb mov %al,-0x5(%rbp) 61c: eb eb jmp 609 <main 0xf> 61e: b8 00 00 00 00 mov $0x0,?x 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)
拷贝到 ?x
,zbl
后缀表示零扩展从 byte 类型到 long 类型,所以只复制低 8位。对 ?x
加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),?x 60f: 3b 45 fc cmp -0x4(%rbp),?x 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,?x 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),?x 60f: 3b 45 fc cmp -0x4(%rbp),?x 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,?x 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),?x 60f: 39 45 fc cmp ?x 60f: 39 45 fc cmp ?x,-0x4(%rbp) 612: 76 06 jbe 61a &l;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
会被强转成无符号类型。