编译器版本
gcc -v
:gcc version 11.2.0 (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders)
cl
:用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.29.30136 版
CL作为微软的非开源编译器,听上去似乎比开源的GNU套件GCC编译器更“高级”,但事实真的如此吗?
咱们统一使用普遍的x64架构,看看两个编译器对同一段C代码的汇编输出有何异同。
统一编译、查看命令
gcc -O0 -c src.c -o gcc_obj.o
cl src.c
// 输出a.obj,但无法链接
反汇编:objdump -d [obj.o] -M intel
// 我更习惯于intel语法,有需要的可以把intel改成att,即输出AT&T语法。
函数调用
研究底层机制,最重要的就是函数调用相关的差异。
栈帧分配
方便起见,写几个具有代表性的函数,但不实现任何实际功能,让汇编代码更简单,更能突出重点。
void func1(void)
{
return;
}
int func2(void)
{
return 0x1234; // 方便反汇编查看
}
int func3(int arg)
{
return arg++;
}
这段代码中,并不包含任何函数调用语句,也就是说它是用来分析函数栈的。
编译,然后分别反汇编,得到如下结果:
gcc结果
0000000000000000 <func1>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 90 nop
5: 5d pop rbp
6: c3 ret
0000000000000007 <func2>:
7: 55 push rbp
8: 48 89 e5 mov rbp,rsp
b: b8 34 12 00 00 mov eax,0x1234
10: 5d pop rbp
11: c3 ret
0000000000000012 <func3>:
12: 55 push rbp
13: 48 89 e5 mov rbp,rsp
16: 89 4d 10 mov DWORD PTR [rbp+0x10],ecx
19: 8b 45 10 mov eax,DWORD PTR [rbp+0x10]
1c: 8d 50 01 lea edx,[rax+0x1]
1f: 89 55 10 mov DWORD PTR [rbp+0x10],edx
22: 5d pop rbp
23: c3 ret
24: 90 nop
25: 90 nop
// 好多个nop(0x90)
2f: 90 nop
可以看见,它严格按照x86_64架构调用约定,在函数头部写上跟32位无差异(仅仅寄存器位数变化)的序言代码,采用基址指针压栈->备份栈顶指针->(开辟栈空间,由于我们的函数都没有局部变量所以没有进行SUB ESP,0x28
)->函数本体->返回值存RAX(这里是EAX,因为改变低32位就能自动高32位清零,但速度更快)->平栈,返回
关注到0x13->0x1f
的代码,翻译成中文即:把RCX里的参数低32位(int)拿出来,放进备用空间,然后从备用空间拿出来,放进EAX寄存器,把EDX换成RAX+1的值,即arg++,然后由于++运算符改变原值,所以需要再把EDX里自增过的值放回去,即使后文没用到了,然后回复基址寄存器并返回。
CL结果
0000000000000000 <func1>:
0: c2 00 00 ret 0x0
3: cc int3
4: cc int3
5: cc int3
6: cc int3
7: cc int3
8: cc int3
9: cc int3
a: cc int3
b: cc int3
c: cc int3
d: cc int3
e: cc int3
f: cc int3
0000000000000010 <func2>:
10: b8 34 12 00 00 mov eax,0x1234
15: c3 ret
16: cc int3
17: cc int3
18: cc int3
19: cc int3
1a: cc int3
1b: cc int3
1c: cc int3
1d: cc int3
1e: cc int3
1f: cc int3
0000000000000020 <func3>:
20: 89 4c 24 08 mov DWORD PTR [rsp+0x8],ecx
24: 48 83 ec 18 sub rsp,0x18
28: 8b 44 24 20 mov eax,DWORD PTR [rsp+0x20]
2c: 89 04 24 mov DWORD PTR [rsp],eax
2f: 8b 44 24 20 mov eax,DWORD PTR [rsp+0x20]
33: ff c0 inc eax
35: 89 44 24 20 mov DWORD PTR [rsp+0x20],eax
39: 8b 04 24 mov eax,DWORD PTR [rsp]
3c: 48 83 c4 18 add rsp,0x18
40: c3 ret
通过func1和func2可以看见,(由于CL默认开始/Od选项,禁止优化),CL编译器确实更高明,发现几乎是个空函数就直接返回,至于那么多int3调试中断不在讨论范围内。
重点看到0x20的func3,首先,把ECX存进备份区,然后开辟0x18的空间(至于为什么是0x18,因为它以为func3里要调用别的函数,所以预先开辟空间,但可以看出0x18模16=8,并不对齐,这是因为call指令会压栈一个0x08字节的返回地址,这就栈对齐了)
然后取值到EAX里面,EAX自增1,放回去,归还栈空间,然后返回。
函数的调用及传参
这个部分按照常理来说,两种编译器应该没多大区别,因为都得遵守64位平台上fastcall调用约定
测试代码:
int func1(int arg)
{
return arg+1;
}
void func2(void)
{
int local_varible = 1;
local_varible = func1(local_varible); // 自增1
}
gcc结果
0000000000000000 <func1>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 89 4d 10 mov DWORD PTR [rbp+0x10],ecx
7: 8b 45 10 mov eax,DWORD PTR [rbp+0x10]
a: 83 c0 01 add eax,0x1
d: 5d pop rbp
e: c3 ret
000000000000000f <func2>:
f: 55 push rbp
10: 48 89 e5 mov rbp,rsp
13: 48 83 ec 30 sub rsp,0x30
17: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
1e: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
21: 89 c1 mov ecx,eax
23: e8 d8 ff ff ff call 0 <func1>
28: 89 45 fc mov DWORD PTR [rbp-0x4],eax
2b: 90 nop
2c: 48 83 c4 30 add rsp,0x30
30: 5d pop rbp
31: c3 ret
32: 90 nop
func2
函数开辟了0x30字节空间,然后使用相对于RBP的寻址(也是一种栈寻址方式),把1
这个值放进对应的局部变量空间,利用EAX寄存器为过渡,取出来,塞进ECX传参,调用,然后把返回值塞回去(这一行代码,完成了)
最后平栈,回复基址,返回。
CL结果
0000000000000000 <func1>:
0: 89 4c 24 08 mov DWORD PTR [rsp+0x8],ecx
4: 8b 44 24 08 mov eax,DWORD PTR [rsp+0x8]
8: ff c0 inc eax
a: c3 ret
// int3 * n
0000000000000020 <func2>:
20: 48 83 ec 38 sub rsp,0x38
24: c7 44 24 20 01 00 00 mov DWORD PTR [rsp+0x20],0x1
2b: 00
2c: 8b 4c 24 20 mov ecx,DWORD PTR [rsp+0x20]
30: e8 00 00 00 00 call 35 <func2+0x15>
35: 89 44 24 20 mov DWORD PTR [rsp+0x20],eax
39: 48 83 c4 38 add rsp,0x38
3d: c3 ret
相比之下CL的代码短得多,它不涉及对RBP的操作,仅仅修改RSP足矣。而且,cl的栈寻址都是采用RSP偏移,更直观(这对于机器来说确实是一句废话),一眼能看出每个局部变量的位置。
这里的func2属于经典调用了……首先开辟常见的0x38空间,存局部变量到对应的栈空间,然后提取到ECX传参,调用,返回,塞回栈空间局部变量区,平栈,退出。文章来源:https://www.toymoban.com/news/detail-446637.html
总结
在最基本的逻辑控制——函数调用方面,CL似乎就比不开优化开关的GCC高明了不少,尤其是指令短,如果实验中的func1,func2被平凡调用,即使每个指令的时钟周期再短,累计起来也是客观的时间。仅从这一个方面看起来,CL就拿时间和空间都换掉了,每个都节省了不少。文章来源地址https://www.toymoban.com/news/detail-446637.html
到了这里,关于Microsoft的CL编译器与GCC到底有什么区别?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!