AT&T
基于x86架构处理器汇编指令一般有2种格式:Intel汇编和AT&T汇编,通过对8086汇编的学习包括对一些常用寄存器以及函数调用非常熟悉,今天我们来学习一下AT&T汇编语言,以及用AT&T汇编语言来探究一些常见代码的执行流程和执行效率。我们通过一张图对比Intel的汇编语法来学习AT&T的基础语法:
通过上面图片对比AT&T汇编与Intel汇编可知,指令后面的操作跟Intel相反,常数前面加$,寄存器前面加%,地址不是用中括号,而是星号标示,指令后面跟的q,l,b均表示操作长度。具体的其它指令请大家自行查阅,前面我们讲了8086的函数汇编调用的完整过程,我们这里来看看AT&T的函数:
int print(int age)
{
int a = 10;
int b = 11;
int c = a + b + age;
return c;
}
int main()
{
int total = print(10);
return 0;
}
//print函数转为汇编如下:
pushq %rbp //保护rbp
movq %rsp, %rbp //保存rsp
movl %edi, -0x4(%rbp) //从寄存器直接取出函数参数赋值到rbp-0x4,就是age
movl $0xa, -0x8(%rbp) //定义一个局部变量,位置在rbp-0x8,就是a
movl $0xb, -0xc(%rbp) //定义一个局部变量,位置在rbp-0xc,就是b
movl -0x8(%rbp), %edi //将a赋值到edi中
addl -0xc(%rbp), %edi //将b跟a相加,结果在edi中
addl -0x4(%rbp), %edi //将agey跟edi相加,结果在edi中
movl %edi, -0x10(%rbp) //将edi赋值到rbp-0x10中,就是c
movl -0x10(%rbp), %eax //这里把结果放入eax中
popq %rbp //恢复bp
retq //函数返回
从以上可以看出函数调用的流程,你可能觉得不对,局部变量直接用内存空间赋值,没有用rsp类似的指针来保护局部变量,这是因为这个函数里面没有调用其他函数,编译器帮我们优化了。,还有个问题函数的参数我们也是知道需要push到栈的,可是这里直接取了!因为AT&T汇编有%rdi、%rsi、%rdx、%rcx、%r8、%r9、%r10等寄存器用于存放函数参数,只有寄存器不够了,再用push参数的方式,参数也是通过rax返回。
for循环
循环遍历的时候我们经常用到,如下:
for(int i = 10;i < 15;i++)
{
printf("%d\n",i);
}
以上代码我们都知道会输出10,11,12,13,14.我们仔细观察for括号里面也有三句代码,然后又有打印代码,可能凭我们的经验还是能知道它的执行顺序,不过最好的还是看看反汇编代码,更保险也更深刻。将上述代码反汇编如下:
0x100001e7f <+15>: movl $0xa, -0x8(%rbp) //变量等于10
0x100001e86 <+22>: cmpl $0xf, -0x8(%rbp) //比较rbp-0x8的值与15的差值
0x100001e8a <+26>: jge 0x100001eb2 //大于等于跳转到0x100001eb2,其实就是跳出循环
-> 0x100001e90 <+32>: movl -0x8(%rbp), %esi // 1
0x100001e93 <+35>: leaq 0x111(%rip), %rdi // 2
0x100001e9a <+42>: movb $0x0, %al // 3
0x100001e9c <+44>: callq 0x100001ef0 //以上1 2 3都是为调用printf函数准备
0x100001ea1 <+49>: movl %eax, -0xc(%rbp) //这里对eax以前的值一个备份
0x100001ea4 <+52>: movl -0x8(%rbp), %eax //取出rbp-0x8的值
0x100001ea7 <+55>: addl $0x1, %eax //值加1
0x100001eaa <+58>: movl %eax, -0x8(%rbp) //然后赋值到rbp-0x8
0x100001ead <+61>: jmp 0x100001e86 // 跳转到 比较rbp-0x8的值与15的差值的汇编指令处
0x100001eb2 <+66>: xorl %eax, %eax //清空eax
通过以上代码可以非常清楚的看出for循环的执行步骤:
1.取出i的值,并且与15比较,满足条件就进行打印操作,否则直接执行步骤3。
2.如果执行了打印,说明依然在循环里面,执行i++后执行步骤1。
3.跳出循环,执行后面的代码。
i++与++i
以前刚学这个i++与++i的时候,大概印象就是i++先加再使用++i则是直接加然后使用,i++不能当成左值,++i能当左值,就是i++不能被赋值,++i可以被赋值。我们先来两个例子:
int main()
{
int i = 10;
int a = i++;
return 0;
}
//转成汇编如下:
pushq %rbp
movq %rsp, %rbp
xorl %eax, %eax
movl $0x0, -0x4(%rbp)
movl $0xa, -0x8(%rbp)
movl -0x8(%rbp), %ecx
movl %ecx, %edx
addl $0x1, %edx
movl %edx, -0x8(%rbp)
movl %ecx, -0xc(%rbp)
popq %rbp
retq
int main()
{
int i = 10;
int a = ++i;
return 0;
}
//转成汇编代码如下:
pushq %rbp
movq %rsp, %rbp
xorl %eax, %eax
movl $0x0, -0x4(%rbp)
movl $0xa, -0x8(%rbp)
movl -0x8(%rbp), %ecx
addl $0x1, %ecx
movl %ecx, -0x8(%rbp)
movl %ecx, -0xc(%rbp)
popq %rbp
retq
我们留意到i++有一句汇编指令是ecx赋值到edx,然后edx+1,再把ecx和edx的值赋值到内存中去,其实就是int a = i++,a只是赋值的是i以前值,i++也是执行了加1,然后赋值到内存去。并不是说int a = i++执行完后,i才加1,反汇编查看就是两个寄存器操作,一个寄存器加了,另一个寄存器没加而已,很多地方说产生了临时变量,个人觉得说法不太严谨,从反汇编看并没有产生新的内存空间,但是是汇编多用了一个寄存器。i++和++i的认识,其实单独执行的时候其实没有什么区别,只是在赋值和运算的时候需要区分。比如我们再来看一个有运算的例子:
int i = 10;
int a = i++ + ++i + i++;
//反汇编代码如下:
movl -0x8(%rbp), %eax//eax = 10
movl %eax, %ecx //ecx =11
addl $0x1, %ecx //ecx = 11
movl %ecx, -0x8(%rbp)
movl -0x8(%rbp), %ecx
addl $0x1, %ecx //ecx = 12
movl %ecx, -0x8(%rbp)
addl %ecx, %eax //eax = 12 + 10 = 22
movl -0x8(%rbp), %ecx
movl %ecx, %edx //ecx = 12 edx = 12
addl $0x1, %edx //edx = 13
movl %edx, -0x8(%rbp) //
addl %ecx, %eax //eax = 12 + 22 = 34
movl -0x8(%rbp), %eax//eax = 10
movl %eax, %ecx //ecx =11
addl $0x1, %ecx //ecx = 11
movl %ecx, -0x8(%rbp)
movl -0x8(%rbp), %ecx
addl $0x1, %ecx //ecx = 12
movl %ecx, -0x8(%rbp)
addl %ecx, %eax //eax = 12 + 10 = 22
movl -0x8(%rbp), %ecx
movl %ecx, %edx //ecx = 12 edx = 12
addl $0x1, %edx //edx = 13
movl %edx, -0x8(%rbp) //
addl %ecx, %eax //eax = 12 + 22 = 34
这个汇编代码,能很好解释这句代码的执行流程,运算符是从左到右,最终结果在eax,edx保存i最终的值,注意eax和ecx的变化。
if else
我们再来窥探下if与else的执行流程,如下所示:
int main()
{
int a = 100;
if(a > 200){
printf("a > 200 %d \n",a);
}else if (a > 150){
printf("a > 150 %d \n",a);
}else if (a > 120){
printf("a > 120 %d \n",a);
}else{
printf("a = other %d \n",a);
}
printf("fun end... \n");
return 0;
}
//转成汇编代码如下:
movl $0x64, -0x8(%rbp)
cmpl $0xc8, -0x8(%rbp)
jle 0x100001dfc ; <+60> at main.cpp:109:17
movl -0x8(%rbp), %esi
leaq 0x182(%rip), %rdi ; "a > 200 %d \n"
movb $0x0, %al
callq 0x100001eb4 ; symbol stub for: printf
movl %eax, -0xc(%rbp)
jmp 0x100001e63 ; <+163> at main.cpp:116:5
cmpl $0x96, -0x8(%rbp)
jle 0x100001e22 ; <+98> at main.cpp:111:17
movl -0x8(%rbp), %esi
leaq 0x169(%rip), %rdi ; "a > 150 %d \n"
movb $0x0, %al
callq 0x100001eb4 ; symbol stub for: printf
movl %eax, -0x10(%rbp)
jmp 0x100001e5e ; <+158> at main.cpp
cmpl $0x78, -0x8(%rbp)
jle 0x100001e45 ; <+133> at main.cpp:114:34
movl -0x8(%rbp), %esi
leaq 0x153(%rip), %rdi ; "a > 120 %d \n"
movb $0x0, %al
callq 0x100001eb4 ; symbol stub for: printf
movl %eax, -0x14(%rbp)
jmp 0x100001e59 ; <+153> at main.cpp
movl -0x8(%rbp), %esi
leaq 0x147(%rip), %rdi ; "a = other %d \n"
movb $0x0, %al
callq 0x100001eb4 ; symbol stub for: printf
movl %eax, -0x18(%rbp)
jmp 0x100001e5e ; <+158> at main.cpp
jmp 0x100001e63 ; <+163> at main.cpp:116:5
leaq 0x13b(%rip), %rdi ; "fun end... \n"
仔细一看其实就是逐个比较,一旦发现符合条件的,执行后就不再比较,跳转到if else外面。这个代码指令执行时间跟我们取得符合条件的位置有关。
switch
接下来我们来看看switch-break的实现逻辑,我们对比下if else 看看它的区别
int main()
{
int a = 100;
switch (a) {
case 98:
printf("a = %d\n",a);
break;
case 99:
printf("a = %d\n",a);
break;
case 100:
printf("a = %d\n",a);
break;
default:
break;
}
printf("fun end... \n");
return 0;
}
//转成反汇编代码如下:
0x100001df0 <+0>: pushq %rbp
0x100001df1 <+1>: movq %rsp, %rbp
0x100001df4 <+4>: subq $0x30, %rsp
0x100001df8 <+8>: movl $0x0, -0x4(%rbp)
0x100001dff <+15>: movl $0x64, -0x8(%rbp)
-> 0x100001e06 <+22>: movl -0x8(%rbp), %eax
0x100001e09 <+25>: movl %eax, %ecx
0x100001e0b <+27>: subl $0x62, %ecx
0x100001e0e <+30>: movl %eax, -0xc(%rbp)
0x100001e11 <+33>: movl %ecx, -0x10(%rbp)
0x100001e14 <+36>: je 0x100001e47 ; <+87> at main.cpp:146:31
0x100001e1a <+42>: jmp 0x100001e1f ; <+47> at main.cpp:144:5
0x100001e1f <+47>: movl -0xc(%rbp), %eax
0x100001e22 <+50>: subl $0x63, %eax
0x100001e25 <+53>: movl %eax, -0x14(%rbp)
0x100001e28 <+56>: je 0x100001e60 ; <+112> at main.cpp:149:31
0x100001e2e <+62>: jmp 0x100001e33 ; <+67> at main.cpp:144:5
0x100001e33 <+67>: movl -0xc(%rbp), %eax
0x100001e36 <+70>: subl $0x64, %eax
0x100001e39 <+73>: movl %eax, -0x18(%rbp)
0x100001e3c <+76>: je 0x100001e79 ; <+137> at main.cpp:152:31
0x100001e42 <+82>: jmp 0x100001e92 ; <+162> at main.cpp:155:13
0x100001e47 <+87>: movl -0x8(%rbp), %esi
0x100001e4a <+90>: leaq 0x152(%rip), %rdi ; "a = %d\n"
0x100001e51 <+97>: movb $0x0, %al
0x100001e53 <+99>: callq 0x100001ee8 ; symbol stub for: printf
0x100001e58 <+104>: movl %eax, -0x1c(%rbp)
0x100001e5b <+107>: jmp 0x100001e97 ; <+167> at main.cpp:157:5
0x100001e60 <+112>: movl -0x8(%rbp), %esi
0x100001e63 <+115>: leaq 0x139(%rip), %rdi ; "a = %d\n"
0x100001e6a <+122>: movb $0x0, %al
0x100001e6c <+124>: callq 0x100001ee8 ; symbol stub for: printf
0x100001e71 <+129>: movl %eax, -0x20(%rbp)
0x100001e74 <+132>: jmp 0x100001e97 ; <+167> at main.cpp:157:5
0x100001e79 <+137>: movl -0x8(%rbp), %esi
0x100001e7c <+140>: leaq 0x120(%rip), %rdi ; "a = %d\n"
0x100001e83 <+147>: movb $0x0, %al
0x100001e85 <+149>: callq 0x100001ee8 ; symbol stub for: printf
0x100001e8a <+154>: movl %eax, -0x24(%rbp)
0x100001e8d <+157>: jmp 0x100001e97 ; <+167> at main.cpp:157:5
0x100001e92 <+162>: jmp 0x100001e97 ; <+167> at main.cpp:157:5
0x100001e97 <+167>: leaq 0x10d(%rip), %rdi ; "fun end... \n"
0x100001e9e <+174>: movb $0x0, %al
0x100001ea0 <+176>: callq 0x100001ee8 ; symbol stub for: printf
0x100001ea5 <+181>: xorl %ecx, %ecx
0x100001ea7 <+183>: movl %eax, -0x28(%rbp)
0x100001eaa <+186>: movl %ecx, %eax
0x100001eac <+188>: addq $0x30, %rsp
0x100001eb0 <+192>: popq %rbp
0x100001eb1 <+193>: retq
break跳转指令是先判断跳转地址再跳转,而不是逐个判断,相当于是空间换时间,效率比较高,如果判断条件比较多的情况下,用switch效率更高。今天就介绍到这里,祝进步!
网友评论