计算机组成原理--计算机指令
编译到汇编,代码从如何变成机器码
看下面的C代码示例:
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
a = a + b;
return 0 ;
}
通过编译和汇编后生成机器码
[aaa@qq.com ~]# gcc -g -c test.c
[aaa@qq.com ~]# ls
anaconda-ks.cfg test.c test.o
[aaa@qq.com ~]# objdump -d -M intel -S test.o
test.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
#include <stdio.h>
int main()
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int a = 1;
4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
int b = 2;
b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
a = a + b;
12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
15: 01 45 fc add DWORD PTR [rbp-0x4],eax
return 0 ;
18: b8 00 00 00 00 mov eax,0x0
}
1d: 5d pop rbp
1e: c3 ret
详细的转换关系如下图所示:
在inter 处理器中大概支持2000多种机器指令集,常见的指令可以分为下面的五类:
IF else 条件指令学习
#include <time.h>
#include <stdlib.h>
int main()
{
srand(time(NULL));
int r = rand() % 2;
int a = 10;
if (r == 0)
{
a = 1;
} else {
a = 2;
}
对应上面if条件的指令如下:
if (r == 0)
3b: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0
3f: 75 09 jne 4a <main+0x4a>
{
a = 1;
41: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
48: eb 07 jmp 51 <main+0x51>
}
else
{
a = 2;
4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
51: b8 00 00 00 00 mov eax,0x0
}
cmp 指令比较了前后两个操作数的值,这里的 DWORD PTR 代表操作的数据类型是 32 位的整数,而 [rbp-0x4] 则是一个寄存器的地址。所以,第一个操作数就是从寄存器里拿到的变量 r 的值。第二个操作数 0x0 就是我们设定的常量 0 的 16 进制表示。cmp 指令的比较结果,会存入到条件码寄存器当中去。
跟着的 jne 指令,是 jump if not equal 的意思,它会查看对应的零标志位。如果为 0,会跳转到后面跟着的操作数 4a 的位置。这个 4a,对应这里汇编代码的行号,也就是上面设置的 else 条件里的第一条指令。当跳转发生的时候,PC 寄存器就不再是自增变成下一条指令的地址,而是被直接设置成这里的 4a 这个地址。这个时候,CPU 再把 4a 地址里的指令加载到指令寄存器中来执行。跳转到执行地址为 4a 的指令,实际是一条 mov 指令,第一个操作数和前面的 cmp 指令一样,是另一个 32 位整型的寄存器地址,以及对应的 2 的 16 进制值 0x2。mov 指令把 2 设置到对应的寄存器里去,相当于一个赋值操作。然后,PC 寄存器里的值继续自增,执行下一条 mov 指令。这条 mov 指令的第一个操作数 eax,代表累加寄存器,第二个操作数 0x0 则是 16 进制的 0 的表示。这条指令其实没有实际的作用,它的作用是一个占位符。我们回过头去看前面的 if 条件,如果满足的话,在赋值的 mov 指令执行完成之后,有一个 jmp 的无条件跳转指令。跳转的地址就是这一行的地址 51。我们的 main 函数没有设定返回值,而 mov eax, 0x0 其实就是给 main 函数生成了一个默认的为 0 的返回值到累加器里面。if 条件里面的内容执行完成之后也会跳转到这里,和 else 里的内容结束之后的位置是一样的。
for 循环指令
int main()
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int a = 0;
4: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
int i;
for( i = 0; i < 5; i ++)
b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0
12: eb 0a jmp 1e <main+0x1e>
{
a += i;
14: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
17: 01 45 fc add DWORD PTR [rbp-0x4],eax
for( i = 0; i < 5; i ++)
1a: 83 45 f8 01 add DWORD PTR [rbp-0x8],0x1
1e: 83 7d f8 04 cmp DWORD PTR [rbp-0x8],0x4
22: 7e f0 jle 14 <main+0x14>
}
return 0;
24: b8 00 00 00 00 mov eax,0x0
}
29: 5d pop rbp
2a: c3 ret
JLE ;有符号小于等于则跳转
JMP;无条件跳转
JNE;不等于侧跳转
switch 指令
int main()
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 48 83 ec 10 sub rsp,0x10
char a = getchar();
8: e8 00 00 00 00 call d <main+0xd>
d: 88 45 ff mov BYTE PTR [rbp-0x1],al
int b = 0;
10: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0
switch(a)
17: 0f be 45 ff movsx eax,BYTE PTR [rbp-0x1]
1b: 83 f8 02 cmp eax,0x2
1e: 74 13 je 33 <main+0x33>
20: 83 f8 03 cmp eax,0x3
23: 74 17 je 3c <main+0x3c>
25: 83 f8 01 cmp eax,0x1
28: 75 1a jne 44 <main+0x44>
{
case 1:
b = 2;
2a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
break;
31: eb 11 jmp 44 <main+0x44>
case 2:
b = 9;
33: c7 45 f8 09 00 00 00 mov DWORD PTR [rbp-0x8],0x9
break;
3a: eb 08 jmp 44 <main+0x44>
case 3:
b = 5;
3c: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x5
break;
43: 90 nop
}
return 0;
44: b8 00 00 00 00 mov eax,0x0
}
49: c9 leave
4a: c3 ret
if 条件多个的判断
if (r== 0)
4c: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0
50: 75 09 jne 5b <main+0x5b>
{
a = 1;
52: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
59: eb 16 jmp 71 <main+0x71>
}else if(r==1){
5b: 83 7d fc 01 cmp DWORD PTR [rbp-0x4],0x1
5f: 75 09 jne 6a <main+0x6a>
a = 2;
61: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
68: eb 07 jmp 71 <main+0x71>
}else{
a = 3;
6a: c7 45 f8 03 00 00 00 mov DWORD PTR [rbp-0x8],0x3
}
}
从上面的汇编语言来看,判断条件大于2个话,使用switch性能更好,还有case分支中越容易发生的放在前面。