反汇编代码里面的地址_浅析同一段C++代码在Win X64, X86,MAC,Android ARM64平台编译器优化之美...

本文通过一个简单的C++代码示例,探讨在Windows X64, X86, MAC和Android ARM64平台上,使用Release模式编译时的编译器优化差异。分析发现,ARM64平台的优化最为出色,启用NENO指令实现高效处理;Windows X64也有显著优化,而MAC和X86则相对保守,但MAC使用了SSE指令集。编译器的优化包括内联函数、调用开销减少和指令集利用。" 24404737,2982959,Python多模块工程中修改其他模块变量的技巧,"['Python编程', '模块化', '程序设计', '变量管理']
摘要由CSDN通过智能技术生成

背景:定位一些Crash崩溃时,由于缺少更多信息,可能需要从反汇编的静态代码段推测对应的C++代码,并结合寄存器值分析出具体原因。对于Release发布版,由于编译器的强行函数内联和生成指令优化,会出现反汇编代码和C++源码区别较大,加大我们从汇编代码反推C++难度,一但我们分析清楚优化点,可以很欣赏编译器优化之美。

本文是从ARM64平台的一次crash反汇编分析经历出发,发现编译器能在我们根本没写判断的情况下,自动增加条件判断,启用128位寄存器的NENO指令进行加速,这是我以前在PC平台从未见过的优化,惊叹之佘,忍不住在我手上仅有的多平台做进一步分析,写一段简单C++代码,同样的代码在Windows下用VS2019选X64,X86,MAC家的XCode C++ 64位 ,Android Studio的ARM64,这四个平台都用Release版本生成,然后用IDA进行静态分析对比,体验一下编译器的强大之处。

我简单写了一个测试样例,C++字符串,加上测试,全部只有60行。为了简化,没有专门的内存分配器,也没有引入一些迭代器,萃取机制,为了简化也有直接用strlen()取长度等不符合模板泛型编程等,但这些不影响分析。无论在那个平台下,都用同样的代码,都会重点分析TestMyString()函数和生成以及差别最大的TCopy()函数。

template<typename T>
T* TCopy(const T* pStart, const T* pFinish, T* pDst)
{
    for (; pStart != pFinish; ++pDst, ++pStart)
    {
        *pDst = *pStart;
    }
    return pDst;
};

template<typename T>
struct TString
{
    ~TString()
    {
        if (_StartPtr) delete _StartPtr;
    }
    TString(const T* pcStr)
    {
        Assign(pcStr, pcStr + strlen(pcStr));
    }
    TString<T>& operator = (const TString<T>& rkRight)
    {
        if (&rkRight != this)
        {
            Assign(rkRight._StartPtr, rkRight._FinishPtr);
        }
        return *this;
    }
    void Assign(const T* pStart, const T* pFinish)
    {
        if (pStart == pFinish || nullptr == pStart)
        {
            _FinishPtr = _StartPtr;
            return;
        }
        int iLen = pFinish - pStart;
        int iSelfSize = _FinishPtr - _StartPtr;
        if (iSelfSize < iLen)
        {
            if (_StartPtr) delete _StartPtr;
            _StartPtr = new char[iLen + 1];
            _EndOfStrore = _StartPtr + iLen;
        }

        _FinishPtr = TCopy<T>(pStart, pFinish, _StartPtr);
        *((char*)_FinishPtr) = '0';
    }
    T* _StartPtr = nullptr;
    T* _FinishPtr = nullptr;
    T* _EndOfStrore = nullptr;
};
using MyString = TString<char>;

void TestMyString()
{
    MyString s1("HelloWrold");
    MyString s2("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
    s1 = s2;
}

windows X64平台:

我们先用VS2019在main函数调用TestMyString(),然后生成windows的X64平台可执行文件,选Release,VS默认速度最快优化,,用IDA反编译工具打开生成的exe,跳到main代码段。好家伙,TestMyString()不见了,直接优化内联展开,省去call,ret开销,且TestMyString里面子函数调用,也全部优化掉,比如

TString<T>& operator = (const TString<T>& rkRight);
void Assign(const T* pStart, const T* pFinish);
T* TCopy(const T* pStart, const T* pFinish, T* pDst);

变成内联,一股脑的全放到main里面了,这也是最常见的优化。

61864364651c466343d711f4e09ac5e4.png

我们重点分析一下:全部汇编代码较长,我们选取部分,也是优化有差别的部分,C++中s1 = s2;这句的反汇编。这本应该调用TString的赋值构造函数,只是优化后全部放在一起了,为了对汇编更好的理解,我对每行都进行注释解析,你只要一行一行看看注释就可以了。如果想深入,可以边看C++代码,边看注释,体验对应关系,感受编译器的优化。

.text:00007FF77B591288                 cmp     rsi, rbx;比较pStart == pFinish
.text:00007FF77B59128B                 jz      short loc_7FF77B5912E6;相等跳转
.text:00007FF77B59128D                 test    rsi, rsi;比较nullptr == pStart
.text:00007FF77B591290                 jz      short loc_7FF77B5912E6;相等跳转
.text:00007FF77B591292                 mov     r14d, ebx;pFinish地址低32位放到r14d中
.text:00007FF77B591295                 sub     r14d, esi;等同int iLen = pFinish - pStart;
.text:00007FF77B591298                 sub     edi, ebp;等同int iSelfSize = _FinishPtr - _StartPtr;
.text:00007FF77B59129A                 cmp     edi, r14d;对iSelfSize和iLen进行比较
.text:00007FF77B59129D                 jge     short loc_7FF77B5912C0; SelfSize>=iLen时跳转
.text:00007FF77B59129F                 test    rbp, rbp; 等同if (_StartPtr) 
.text:00007FF77B5912A2                 jz      short loc_7FF77B5912B1;本例非空,不直接跳
.text:00007FF77B5912A4                 mov     edx, 1;为delete传第2个参数
.text:00007FF77B5912A9                 mov     rcx, rbp;为delete传第1个参数,也是地址
.text:00007FF77B5912AC                 call    ??3@YAXPEAX_K@Z ; 调用delete;
                                      ;operator delete(void *,unsigned __int64)
.text:00007FF77B5912B1 loc_7FF77B5912B1:                       ; CODE XREF: main+E2↑j
.text:00007FF77B5912B1                 lea     eax, [r14+1];等同取(iLen + 1)值的地址
.text:00007FF77B5912B5                 movsxd  rcx, eax ;为new传参,size
.text:00007FF77B5912B8                 call    ??_U@YAPEAX_K@Z ; 调operator new[](unsigned __int64)
.text:00007FF77B5912BD                 mov     rbp, rax;等同_StartPtr = new char[iLen + 1];
.text:00007FF77B5912C0 loc_7FF77B5912C0:                       ; CODE XREF: main+DD↑j
.text:00007FF77B5912C0                 mov     r8, rbp;_StartPtr放入r8
.text:00007FF77B5912C3                 mov     rdx, rsi;pStart放入rdx
.text:00007FF77B5912C6                 db      66h, 66h
.text:00007FF77B5912C6                 nop     word ptr [rax+rax+00000000h];空指令
.text:00007FF77B5912D0
.text:00007FF77B5912D0 loc_7FF77B5912D0:                       ; CODE XREF: main+120↓j
.text:00007FF77B5912D0                 movzx   eax, byte ptr [rdx];取pStart对应一个字节数据到eax
.text:00007FF77B5912D3                 mov     [r8], al;让一个字节字符放到_StartPt中
.text:00007FF77B5912D6                 lea     r8, [r8+1];等同++_StartPt
.text:00007FF77B5912DA                 inc     rdx;等同++pStart
.text:00007FF77B5912DD                 cmp     rdx, rbx;比较pStart与 pFinish
.text:00007FF77B5912E0                 jnz     short loc_7FF77B5912D0;不相等,跳转,完成循环拷贝
.text:00007FF77B5912E2                 mov     byte ptr [r8], 0;等同*((char*)_FinishPtr) = '0';

X64总结:非常强的调用优化,除了new,delete系统函数,减少全部子函数的call,ret开销,其中内联TCopy(const T* pStart, const T* pFinish, T* pDst)代码区块,三个参数中pFinish算是略去(利用很早rbx就保存了),没有照本宣科。也能知道T是POD类型,没有单独的赋值构造,也是比较巧秒的。


windows X86平台:

操作同X64,,用IDA反编译工具打开生成的exe,一看哦呵,这优化有点弱啊,虽然代码比较简单,但不像x64代码,所有函数一股脑优化到main中,win32函数跳转都没有省去。我又仔细对比了VS工程C++优化选项卡,发现win64对于omit-frame-pointer是空的,查所有参数也没有标明“/Oy-”或者“Oy”;x86默认是否的,我手动打开吧,不然更比不过x64了。

e146ebb6637cee37da937be9458af583.png

启用omit-frame-pointe,重新编译生成。用IDA打开,首先发现x86代码虽然大部分也直接优化到main中,但一些函数调用还是没有优化掉,比如TString<char>::Assign(char const *,char const *),其函数里生成了很多条的指令,另外就是和C++写法非常接近,几乎一行对一行,很容易反汇编。

473c158901fbc3babbf868076f685122.png

受限于篇幅,加上和X64代码非常的像,我们只分析最核心的,和后面ARM平台差别比较大的一部分汇编,下面汇编代码对应TString<char>::Assign(char const *,char const *)部分。

.text:004012F7                 push    eax             ; 为new传参,size
.text:004012F8                 call    ??_U@YAPAXI@Z   ; 调operator new[](uint)
.text:004012FD                 mov     edx, eax;得到新申请的内存放到edx中
.text:004012FF                 add     esp, 4;维护栈
.text:00401302                 mov     [edi], edx;edi地址相当于对s1的对象地址,内存传入s1._StartPtr
.text:00401304                 lea     this, [edx+ebp];等同_StartPtr + iLen放到this
.text:00401307                 mov     [edi+8], this;等同s1._EndOfStrore = this
.text:0040130A
.text:0040130A loc_40130A:                            
.text:0040130A                 pop     ebp;维护栈
.text:0040130B                 nop     dword ptr [eax+eax+00h];空指令
.text:00401310 loc_401310:                             
.text:00401310                 mov     al, [esi]取pStart对应一个字节数据到al
.text:00401312                 inc     esi;增加pStart地址
.text:00401313                 mov     [edx], al;让一个字节字符放到_StartPt中
.text:00401315                 inc     edx;增加_StartPt地址
.text:00401316                 cmp     esi, ebx;比较pStart与 pFinish
.text:00401318                 jnz     short loc_401310;不相等,跳转,完成循环拷贝
.text:0040131A                 mov     [edi+4], edx;_FinishPtr =TCopy(pStart, pFinish, _StartPtr);
.text:0040131D                 pop     edi;维护栈
.text:0040131E                 pop     esi;维护栈
.text:0040131F                 mov     byte ptr [edx], 0;等同*((char*)_FinishPtr) = '0';
.text:00401322                 pop     ebx;维护栈
.text:00401323                 retn    8;结束

X86总结:调用优化还算可以,除了new,delete系统函数,只有TString<char>::Assign(char const *,char const *)一个函数的call,ret开销,在和x64的指令具体对比中,优化度明显不如x64,过于像C++一行一行的翻译,且引如较多栈平衡,关键的拷贝也不够精练。


MAC平台:

MAC平台用XCode生成64位的Release可执行程序,工程全部默认值,没有开单独的优化。发现和X86差不多的水平,函数调用开销没有怎么省,该call还是call,而且有更多的栈保护与平衡,甚至弱于x86,虽是64位,但一点也没有像X64那么激进。

b96447a4a22abe0e54ffa716c2e904d3.png

由于MAC下生成执行程序的反汇编和x86差不多代码,再注释也是重复的,我们就不一行一行的分析了,有兴趣的可以看IDA。不过发现这里有个亮点,就是MAC启用SSE指令集,使用XMM的128位寄存器,属于SIMD,还是可以的。摘录一下这段代码,并注释,来自TestMyString()函数。

__text:0000000100006B47                 xorps   xmm0, xmm0;清空xmm0寄存器
__text:0000000100006B4A                 lea     rdi, [rbp+var_40];取栈上变量s1的地址
__text:0000000100006B4E                 movaps  xmmword ptr [rdi], xmm0;使用128位的xmm0,直接将s1的
                         ;s1._StartPtr,s1._FinishPtr初始化为0,SIMD操作,妙哉妙哉
__text:0000000100006B51                 xor     ebx, ebx;ebx清零
__text:0000000100006B53                 mov     [rdi+10h], rbx;s1._EndOfStrore初始化为0

总结:函数调用方面比X86还保守点,没有太多亮点,但居然自己动使用了SSE指令集,128位的XMM0直接清零两个指针,SIMD操作,也是不单手动开其它可选优化时的亮点。


Android的ARM64平台:

采用Android Studio,选择Native C++工程,将前面的C++代码放到cpp,选择Release版本,Arm64平台,编译生成.so运行库二进制代码。为了这个我专门IDA动态反调试一把Release的APK,两个字,舒服。不过我也是刚接触IDA和Android开发,也不太熟悉,所以才惊叹它的优化,可能后面就会麻木。

5a36a4c03ff6a1a9ab4299f93574c119.png

这边我就静态分析了,总的看起来非常强的调用优化,除了new,delete系统函数,减少全部子函数的调用开销全部优化掉,也是一股脑全部放到一起,加大反推C++难度,和X64差不多水平,但细节没有x64秒。

下面说这里面有个非常强的优化,那就是启用NENO指令,由于有前面三个平台那么多分析,特别通用的地方就不再赘述,下面对部分汇编代码进行分析,实际是T* TCopy(const T* pStart, const T* pFinish, T* pDst)的代码,这个优化在拷大字符串,还是真香,尤其真机Release调试一下,妙。我也用Android Studio生成Deubg版本,实机也调试过,发现没有这段NENO优化。

NEON 技术可加速多媒体和信号处理算法(如视频编码/解码、2D/3D 图形、游戏、音频和语音处理、图像处理技术、电话和声音合成),其性能至少为ARMv5 性能的3倍,为 ARMv6 SIMD性能的2倍。本文是用了128位的Q0,Q1,由于有前面三个平台那么多分析,特别通用的地方就不再赘述,下面对部分汇编代码进行分析。

.text:000000000000EC10                 ADRP            X9, #aHellowrold@PAGE ; "HelloWrold"
.text:000000000000EC14                 CMP             X20, #0x20;字符串大小和32比较,这个我们根本
;没有写这样的代码,编译器自己加的条件判断,32是因为它用NENO指令,一次可以取256bit数据,非常强
.text:000000000000EC18                 ADD             X9, X9, #aHellowrold@PAGEOFF ; "HelloWrold"
.text:000000000000EC1C                 B.CS            loc_EC2C;如果大于32跳转EC2C
.text:000000000000EC20                 MOV             X22, X19
.text:000000000000EC24                 MOV             X8, X9
.text:000000000000EC28                 B               loc_EC64
.text:000000000000EC2C
.text:000000000000EC2C loc_EC2C                              ; CODE XREF: TestMyString(void)+68↑j
.text:000000000000EC2C                 AND             X10, X20, #0xFFFFFFFFFFFFFFE0;这里为了32对齐
.text:000000000000EC30                 ADD             X11, X9, #0x10;x11为字符串源地址加16
.text:000000000000EC34                 ADD             X22, X19, X10;x22为对齐后字符串大小
.text:000000000000EC38                 ADD             X8, X9, X10;x8为不能用NENO拷的字符串地址
.text:000000000000EC3C                 ADD             X12, X19, #0x10;x12目前为16
.text:000000000000EC40                 MOV             X13, X10;可以加速大小
.text:000000000000EC44
.text:000000000000EC44 loc_EC44                              ; CODE XREF: TestMyString(void)+A4↓j
.text:000000000000EC44                 LDP             Q0, Q1, [X11,#-0x10];一次取256bit的字符串
.text:000000000000EC48                 ADD             X11, X11, #0x20;字符串源地址加32
.text:000000000000EC4C                 SUBS            X13, X13, #0x20;可加速大小减32
.text:000000000000EC50                 STP             Q0, Q1, [X12,#-0x10];放256bit到目标内存
.text:000000000000EC54                 ADD             X12, X12, #0x20;目标地址加32
.text:000000000000EC58                 B.NE            loc_EC44;不为空,循环
.text:000000000000EC5C                 CMP             X20, X10;比较是否相等,就是还有没有剩余,
;如果有剩余,就调用最普通单字节拷贝,这里就不再分析了

总结:和X64差不多一样的优化水平,还引用NENO指令加速,一次处理256bit数据,自动增加和32条件判断,第一次见,太强了,秒秒秒!

综述:这个简单的测试C++样本,ARM64编译表现最强,优化综合也是最强的,启用NENO指令,自动增加条件语句,一次处理32个字符,完全吊打的级别。如果没有NENO指令,Windnows下X64可以说优化最美最强的。剩下MAC和X86都差不多,不过MAC还是用了一点点SSE,采用SIMD,至少128bit操作吧,还算亮点吧。两者基本都是照C++一行一行翻译,中规中矩。

注:本文原创,不足之处,欢迎点评。编译器优化和开发软件相关也和目标平台有关,同样代码,不同版本的开发软件生成代码也不同,同一平台,同一软件,编译选项也有非常大的关系,本文均是采用Release版的默认设置,除了X86默认下太拉跨,手动开了omit-frame-pointer。这里不是说那个编译器好那个差,只是从汇编层面对比一下不同平台C++生成二进制代码的美妙之处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值