Microsoft的CL编译器与GCC到底有什么区别?

这篇具有很好参考价值的文章主要介绍了Microsoft的CL编译器与GCC到底有什么区别?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

编译器版本

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传参,调用,返回,塞回栈空间局部变量区,平栈,退出。

总结

在最基本的逻辑控制——函数调用方面,CL似乎就比不开优化开关的GCC高明了不少,尤其是指令短,如果实验中的func1,func2被平凡调用,即使每个指令的时钟周期再短,累计起来也是客观的时间。仅从这一个方面看起来,CL就拿时间和空间都换掉了,每个都节省了不少。文章来源地址https://www.toymoban.com/news/detail-446637.html

到了这里,关于Microsoft的CL编译器与GCC到底有什么区别?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Ubuntu中安装gcc编译器

    文章目录 前言 一、gcc编译器是什么? 二、使用步骤 1.Ubuntu安装gcc编译器 2.使用gcc编译器生成可自行文件 在Ubuntu中是可以直接编译C语言文件的,需要安装gcc编译器,在Ubuntu中默认是没有安装gcc编译器需要自己手动安装,安装完直接就可以使用 build-essential就是gcc的安装包,提

    2024年02月08日
    浏览(44)
  • Linux编译器gcc/g++

    以gcc编译 以g++编译,但是此时会发现没有g++这个指令,所有需要安装它,安装指令 yum install gcc gcc-c++ gcc和g++都会形成可执行文件a.out gcc只能编译c语言代码,g++能编译c/c++ 以c程序为例,来看看它从一个文本类的c程序编译成计算机可以认识的二进制程序它需要经过四个阶段 预

    2024年02月10日
    浏览(44)
  • 【Linux】03 GCC编译器的使用

     在使用gcc编译程序时,编译过程可以简要划分为4个阶段:         预处理、编译、汇编、链接 这个阶段主要处理源文件中的#indef、#include和#define预处理命令; 这里主要是把一些include的头文件和一些宏定义,放到源文件中。 编译命令: gcc  -E  -o  hello.i  hello.c 将经过预处

    2024年01月20日
    浏览(55)
  • Linux编译器——gcc/g++使用

    前言:  在上一篇,我们学习了关于文本编辑器 vim 的全部知识,今天给大家带来的是关于Linux编译器—gcc/使用的详细介绍。 本文目录  (一)温习程序的产生的过程 1、前言 2、程序的产生过程 3、🌜初步认识 gcc🌛 a) gcc的基本概念 b)gcc的基本特点 4、使用方法💻 (二)

    2023年04月17日
    浏览(55)
  • Linux--编译器-gcc/g++使用

    目录 前言 1.看一段样例  2.程序的翻译过程 1.第一个阶段:预处理 2.第二个阶段:编译 3.第三个阶段:汇编 4.第四个阶段:链接 3.程序的编译为什么是这个样子? 4. 关于编译器 5.链接(动静态链接) 1.首先,我们来看一段样例(见一下) 2.见完之后,我们来看一下程序的翻译

    2024年02月20日
    浏览(63)
  • 【Linux】编译器-gcc/g++使用

    个人主页 : zxctscl 文章封面来自:艺术家–贤海林 如有转载请先通知 在之前已经分享了 【Linux】vim的使用,这次来看看在云服务器上的编译器gcc。 我们先写一段简单的代码: 当我们进行编译的时候: 发现根本就编译不了。 这个是因为编译器版本的问题: 查看编译器的版

    2024年03月11日
    浏览(129)
  • Linux编译器 gcc与g++

    程序的编译过程: 1、 预处理 (头文件包含、消除注释、宏定义替换) 2、 编译 (将语言替换成汇编代码) 3、 汇编 (将汇编指令转换为二进制指令) 4、 链接 (合并段表、符号表合并及重定位) 我们可以通过gcc工具实现程序的编译过程: 2.1 预处理 预处理会完成:①头

    2023年04月18日
    浏览(74)
  • QT使用MSVC编译器报错:Project ERROR: Cannot run compiler ‘cl‘以及后续问题解决

    记录一次qtcreator工程使用MSVC创建报错“ Project ERROR: Cannot run compiler ‘cl’ ”,没办法运行编译器cl。 只显示 qmake配置文件 不出现工程文件, 在qtcreator安装目录下的MSVC2017确实并未找到cl.exe文件,于是下载了everything搜索软件,在电脑上找到了VSstudio的安装目录下MSVC2017编译器下

    2024年02月05日
    浏览(64)
  • Linux的编译器——gcc/g++(预处理、编译、汇编、链接)

    前言: 本文主要认识与学习 Linux 环境下常用的编译器—— gcc (编译 C 代码)/ g++ (编译 C++ 代码)的常用指令等、程序实现的两大环境、动态库与静态库的理解等。 任何一个 C 程序的实现都要经过 翻译环境 与 执行环境 。 在翻译环境中又分为4个部分, 预编译、编译、汇

    2024年02月13日
    浏览(46)
  • Ubuntu 用gcc/CMakefile编译器 编译、运行c语言程序

    目录 一. 在Ubuntu系统下用c语言编写一个简单的输出hello world 的程序,并编译、运行。 1.1 gcc/g++简介 1.2 c++程序输出 Hello World: 1.3 c语言程序输出 Hello World: 二. 编写一个主程序文件main1.c和一个子程序文件sub1.c,实现函数间的调用 2.1  编写sub1.c 和main1.c 函数 2.1.1 编写sub1.c函数

    2024年02月04日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包