C/C++ 的一個符號操作問題

2019-5-14 19:54:52
原文鏈接

今天發現一串奇異的代碼,和師兄們一起討論研究之後,彙總成這篇文章。

首先先亮出代碼:

//a.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    int a=10;
    //注意下面的操作
    
    a+=a-=a*=a;
    printf("%d\n", a);
    return 0;
}
//gcc 和g++編譯結果都是0

上述代碼g++gcc編譯結果都是0;使用
gcc -S a.c -o gcc_a.sg++ -S a.c -o g_a.s得到其彙編代碼相同如下:

    .file   "a.c"
    .section    .rodata
.LC0:
    .string "%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $10, -4(%rbp)
    movl    -4(%rbp), %eax
    imull   -4(%rbp), %eax
    movl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    subl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    addl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

接下來就是對這一串彙編代碼的解讀了。

GCC指令

參考鏈接: GCC;GCC online documentation;GCC C++ Command Options;GCC參數詳解

GCC 編譯步驟

參考鏈接: gcc程序的編譯過程和鏈接原理;

GCC編譯分為4步;

編譯流程圖

  1. gcc -E:預處理階段(preprocessing),對包含的頭文件(#include)和宏定義(#define,#ifdef等)進行處理。在上述的代碼處理過程中,編譯器將包含的頭文件stdio.h編譯進來,並且讓用户使用選項”-E“
    進行查看,該選項的作用是讓gcc在預處理結束後停止編譯過程。”.i“文件為已經過預處理的c程序。一下列出部分;處理操作: gcc -E a.c -o gcc_a_e.i -std=c99
  2. gcc -S:編譯操作(compilation),將C/C++或者.i文件編譯成為彙編代碼。處理操作:gcc -S gcc_a_e.i -o gcc_a_e.s -std=c99
  3. gcc -c:彙編操作(assembly),將.c或者.s文件轉化為對應的二進制文件.o或者.obj;處理操作:gcc -c gcc_a_e_s.s -o gcc_a.o;
  4. gcc -o a a.o:鏈接操作(link);鏈接不同的.o文件,生成可執行的文件。

常用選項表

選項含義
-v查看gcc編譯器的版本,顯示gcc執行時的詳細過程
-o <file>Place the output into ;指定輸出文件名為file,這個名稱不能跟源文件名同名
-EPreprocess only; do not compile, assemble or link;只預處理,不會編譯、彙編、鏈接
-SCompile only; do not assemble or link;只編譯,不會彙編、鏈接
-cCompile and assemble, but do not link; 編譯和彙編,不會鏈接
-On編譯優化選項,默認等級-O1,-O2告訴GCC除了完成-O1級別的優化之外,同時還要進行一些額外的調整工作,如處理器指令調度等,選項-O3則除了完成-O2級別的優化之外,還包括循環展開和其他一些與處理器特性相關的優化工作
-l-lLIBRARY 連接時搜索指定的函數庫LIBRARY
-IInclude 添加include頭文件
-Ldir制定編譯的時候,搜索庫的路徑。比如你自己的庫,可以用它制定目錄,不然編譯器將只在標準庫的目錄找。這個dir就是目錄的名稱。
-w/Wall不生成/生成所有警告信息

鏈接原理

使用 gcc -V -o gcc_a_e gcc_a_e.o查看鏈接詳細操作如下:

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.11' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11) 
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'gcc_a_e' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccXTxtp9.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o gcc_a_e /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. gcc_a_e.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

注意:

  • crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o是gcc加入的系統標準啓動文件,對於一般應用程序,這些啓動是必需的。
  • -lc:鏈接libc庫文件,其中libc庫文件中就實現了printf等函數
  • gcc默認使用動態鏈接庫進行鏈接,生成的程序在執行的時候需要加載所需要的動態庫才能運行,動態庫體積小,但是必須依賴所需的動態庫否則無法執行。
  • 添加-static選項可以實現,靜態庫的鏈接,生成的程序包含所需要的所有庫可以直接運行。不過體積較大。操作:gcc -v -static -o a_static a.c.
  • -nostartfiles:不鏈接標準啓動庫文件,而標準庫文件仍然可以正常使用。
  • -nostdlib:不鏈接系統標準啓動文件和標準庫文件。

生成彙編指令結果:

gcc -S -O3 -o a1.s a.c

//a1.s

    .file   "a.c"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "%d\n"
    .section    .text.unlikely,"ax",@progbits
.LCOLDB1:
    .section    .text.startup,"ax",@progbits
.LHOTB1:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB23:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    xorl    %edx, %edx
    movl    $.LC0, %esi
    movl    $1, %edi
    xorl    %eax, %eax
    call    __printf_chk
    xorl    %eax, %eax
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE23:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE1:
    .section    .text.startup
.LHOTE1:
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

Intel x86 基本彙編指令

參考鏈接: 深入淺出GNU X86-64 彙編;x86彙編程序基礎(AT&T語法)彙編語言入門教程

在此簡單的介紹x86_64基本彙編指令,不做過多介紹。

程序加載時系統的內存分配

程序啓動過程:
程序-------> 程序加載器| ----------> Flash memory
| ----------> SDRAM
| ----------> BBRAM

系統區塊特性

名稱種類
SHT_NULL無效的區塊
SHT_PROGBITS帶有數據(機械語和初始值等)的區塊
SHT_NOBITS不帶有數據
SHT_RELA帶有可再分配的數據
SHT_REL帶有可再分配的數據且不依賴與內存的代碼
SHT_SYMTAB帶有符號表的區塊
名稱屬性
SHF_ALLOC應該放在內存上的區塊
SHF_WRITE應該放在可讀寫區域的區塊
SHF_EXECINSTR應該放在可執行區域的區塊

歸類

文件種類屬性説明
.bbsSHT_NOBITSSHF_ALLOC + SHF_WRITE主要存放0或者無初始值的全局變量和0或者無初始值的靜態局部變量
.dataSHT_PROGBITSSHF_ALLOC + SHF_WRITE主要存放初始值是0以外的全局變量和初始值為0以外的靜態局部變量
.textSHT_PROGBITSSHF_ALLOC + SHF_EXECINSTR機械語跟代碼
.rodataSHT_PROGBITSSHF_ALLOC字符串,或者定數(const)

linux各個字段含義:
參考鏈接: Linux段管理

字段名稱意義
.bbs/BSSBSS是英文Block Started by Symbol的簡稱。屬於靜態內存分配;用於存儲未初始化的全局變量或者是默認初始化為0的全局變量,它不佔用程序文件的大小,可是佔用程序執行時的內存空間。
.data/data該段用於存儲初始化的全局變量,初始化為0的全局變量出於編譯優化的策略還是被保存在BSS段
.rodata/rodata該段也叫常量區,用於存放常量數據,ro就是Read Only之意。1.些馬上數與指令編譯在一起直接放在代碼段。2.對於字符串常量,編譯器會去掉反覆的常量,讓程序的每一個字符串常量僅僅有一份。3.有些系統中rodata段是多個進程共享的,目的是為了提高空間的利用率。
.text/text用於存放程序代碼的,編譯時確定,僅僅讀。更進一步講是存放處理器的機器指令,當各個源文件單獨編譯之後生成目標文件。經連接器鏈接各個目標文件並解決各個源文件之間函數的引用,與此同一時候,還得將全部目標文件裏的.text段合在一起,但不是簡單的將它們“堆”在一起就完事,還須要處理各個段之間的函數引用問題。
.stack/stack是由系統負責申請釋放,其操作方式類似stack,用於存儲參數變量及局部變量,事實上函數的運行也是stack的方式,所以才有了遞歸
.heap/heap它由用户申請和釋放。申請時至少分配虛存,當真正存儲數據時才分配對應的實存,釋放時也並不是馬上釋放實存。而是可能被反覆利用。

64位內存佈局

彙編程序基本元素組成

  • 指示(Directives):以點號開始,用來指示對編譯器,連接器,調試器有用的結構信息。指示本身不是彙編指令。例如:
    • .file:只是記錄原始源文件名。
    • .data:表示數據段(section)的開始地址。
    • .text:表示實際程序代碼的起始。
    • .string:表示數據段中的字符串常量。
    • .globl main:指明標籤main是一個可以在其它模塊的代碼中被訪問的全局符號。
    • .section :定義內存段,聲明段的類型,例如.section .rodata
      • .bbs
      • .rodata
      • .text
      • .stack
      • .heap
  • 標籤(Labels):以冒號結尾,用來把標籤名和標籤出現的位置關聯起來。按照慣例, 以點號開始的標籤都是編譯器生成的臨時局部標籤,其它標籤則是用户可見的函數和全局變量名稱。例如:
    • .LC0::表示緊接着的字符串的名稱是.LC0.標籤。
    • .cfi_ 開頭的彙編指示符用來告訴彙編器生成相應的 DWARF 調試信息(.cfi指令解讀;官方文檔)
      • .cfi_startproc:定義函數開始
      • .cfi_endproc:定義函數結束
      • .cfi_def_cfa_offset:定義此處距離CFA(Canonical Frame Address)標準框架地址–在前一個調用框架中調用當前函數時的棧頂指針
    • main::表示指令 pushq %rbp是main函數的第一個指令。
  • 指令(Instructions):實際的彙編代碼 (pushq %rbp), 一般都會縮進,以便和指示及標籤區分開來。

CPU寄存器

X86-64中,所有寄存器都是64位,相對32位的x86來説,標識符發生了變化,比如:從原來的%ebp變成了%rbp。為了向後兼容性,%ebp依然可以使用,不過指向了%rbp的低32位。X86-64寄存器的變化,不僅體現在位數上,更加體現在寄存器數量上。新增加寄存器%r8到%r15。加上x86的原有8個,一共16個寄存器。寄存器集成在CPU上,存取速度比存儲器快好幾個數量級,寄存器多了,GCC就可以更多的使用寄存器,替換之前的存儲器堆棧使用,從而大大提升性能。

x86_64寄存器地址

注意:隨着設計的進展,新的指令和尋址模式被添加進來,使得很多寄存器變成了等同的。少數留下來的指令,特別是和字符串處理相關的,要求使用%rsi%rdi。兩個寄存器被保留下來分別作為棧指針 (%rsp) 和基址指針 (%rbp)。最後的8個寄存器是編號的並且沒有特殊限制。

多年來,體系結構從8位擴展到16位,32位,因此每個寄存器都有一些內部結構:

%rax 寄存器內部結構

%rax的低8位是8位寄存器%al,僅靠的8位是%ah。低16位是%ax,低32位是%eax,整個64位是%rax。
寄存器%r8-%r15也有相同結構,但命名方式稍有不同:

%r8-%r15寄存器內部結構

注意:大多數編譯器產品混合使用32和64位模式。32位寄存器用來做整數計算,因為大多數程序不需要大於 2^32 的整數值。64位一般用來保存內存地址(指針),使得可以尋址到16EB虛擬內存。

尋址模式

x86_64使用複雜指令集(CISC)(risc與cisc)所以MOV指令有很多不同的變種以便在不同的存儲單元之間移動不同的數據類型。與大多數指令相同,有着決定移動大多數數據的單字母前綴:

單字母前綴

不同的數據有着不同的尋址模式和內存偏移:

  • 全局值(全局變量和函數)的引用直接使用名字,例如x或者printf
  • 常數使用帶有$的立即數,例如$56。
  • 寄存器值的引用:使用寄存器的名稱,例如:%rbx
  • 間接尋址:使用與寄存器中保存的地址值相對應的內存中的值,例如:(%rsp)表示%rsp指向的內存中的值。
  • 相對基址尋址:把一個常數加到寄存器上,例如-16(%rcx)表示把%rcx指向的地址前移16個字節後對應的內存值。-16(%rbx,%rcx8)表示-16+%rbx+%rcx*8對應的地址的內存值。

使用各種尋址模式加載一個64位值到%rax:

添加一個64位值到%rax

注意:
注意GNU工具使用傳統的AT&T語法。類Unix操作系統上,AT&T語法被用在各種處理器上。Intel語法則一般用在DOS和Windows系統上。下面是AT&T語法的指令:

movl %esp, %ebp

movl是指令名稱。%則表明esp和ebp是寄存器.在AT&T語法中, 第一個是源操作數,第二個是目的操作數。
在其他地方,例如interl手冊,你會看到是沒有%的intel語法,它的操作數順序剛好相反。下面是Intel語法:

MOVQ EBP, ESP

當在網頁上閲讀手冊的時候,你可以根據是否有%來確定是AT&T 還是 Intel 語法。

基本算術指令

編譯器會使用基本的算術指令:addsubimulidiv.其中ADDSUB有兩個操作數:

  • add:加操作;例:ADDQ %rbx, %ras;把%rbx加到%rax,結果存在%rax中,會覆蓋之前的值。值的內存大小為8bit
  • sub:減操作;例如SUBQ %rbx,%rax;%rax值存儲減去%rbx,結果存儲在%rax中。
  • imul:乘操作;例如IMULQ %rbx %rax;它把%rax的值乘以操作數,把結果的低64位存在%rax,高64位放在%rdx(兩個64位值相乘的結果是128位)。
  • idiv:除操作:把128bit值(低64位在 %rax ,高64位在%rdx)除以指令中的操作數為了正確處理負數,用CDQO 指令把%rax符號擴展到%rdx),商存儲在%rax,餘數在%rdx。
  • AND:布爾操作,與
  • OR:布爾操作,或
  • OR:布爾操作,非
  • jmp:跳轉指令;跳轉到制定地點。
  • 跳轉比較指令。比較並跳轉

比較跳轉指令

//%rax從0累加到5:

        MOVQ $0, %rax
loop:
        INCQ %rax
        CMPQ $5, %rax
        JLE  loop

  • inc:目標操作數+1;使得目標操作數添加1.
  • dec:目標操作數-1;使得目標操作數減1

棧 statck

棧是一個輔助的數據結構,主要用來記錄函數的調用歷史和相關的局部變量(沒有放到寄存器的)。一般棧是從高地址到低地址向下生長的。%rsp是棧指針,指向棧最底部(其實是平常所説的棧頂)元素。所以,push %rax(8字節),會把%rsp減去8,並把%rax寫到 %rsp指向的位置。

  • push:
SUBQ $8, %rsp #%rsp指針指向地址減去8個字節
MOVQ %rax, (%rsp) #將%rax移動到%rsp指向地址

  • pop:操作與push剛好相反
MOVQ (%rsp), %rax #將%rsp指針指向值,移動到%rax
ADDQ $8, %rsp #%rsp指針值減8

函數調用

參考鏈接: 32位彙編語言學習筆記(13)–函數的調用;數據傳送指令詳解

在大多數彙編程序中(X86-64不是),調用約定是簡單的把每個參數都壓棧,然後調用函數。被調用的函數從棧中獲取參數,完成操作,把返回值保存到寄存器中並返回。調用方再把參數從棧pop出來(其實X86 32就是這樣的)。

X86-64(System V ABI)。 基本方法:

  • 整數參數(包含指針)依次放在%rdi, %rsi, %rdx, %rcx, %r8, 和 %r9 寄存器中。
  • 浮點參數依次放在寄存器%xmm0-%xmm7中。
  • 寄存器不夠用時,參數放到棧中。
  • 可變參數哈函數(比如printf), 寄存器%eax需記錄下浮點參數的個數。
  • 被調用的函數可以使用任何寄存器,但它必須保證%rbx, %rbp, %rsp, and %r12-%r15恢復到原來的值(如果它改變了它們的值)。
  • 返回值存儲在 %eax中.

基本函數指令

  • call: 調用函數指令;例如:call main;調用main函數
  • ret:終止當前函數的執行,將運行權交還給上層函數。當前函數的幀將被回收

函數調用操作方法

調用函數前,先要把參數放到寄存器中。然後,調用方要把寄存器%r10 和%r11的值保存到棧中。之後,執行CALL指令,把IP指針的值保存到棧中,並跳到函數的起始地址執行。從函數返回後,恢復%r10 和%r11,並從%eax獲取返回值。下面進行一個函數舉例:

nt func( int a, int b, int c )
{
        int x, y;
        x = a+b+c;
        y = x*5;
        return y;
}

對下面的複雜函數有:

.globl func #定於全局函數
func:
    ##################### preamble of function sets up stack
#int a, int b, int c
    pushq %rbp          # save the base pointer
    movq  %rsp, %rbp    # set new base pointer to esp

    pushq %rdi          # save first argument (a) on the stack
    pushq %rsi          # save second argument (b) on the stack
    pushq %rdx          # save third argument (c) on the stack

    subq  $16, %rsp     # allocate two more local variables

    pushq %rbx          # save callee-saved registers
    pushq %r12
    pushq %r13
    pushq %r14
    pushq %r15

    ######################## body of function starts here

    movq  -8(%rbp),  %rbx  #a # load each arg into a scratch register
    movq  -16(%rbp), %rcx  #b
    movq  -24(%rbp), %rdx  #c

    addq  %rdx, %rcx   #b=b+c    # add the args together 
    addq  %rcx, %rbx   #a=a+b
    movq  %rbx, -32(%rbp)   # store the result into local 0 (x)

    movq  -32(%rbp), %rbx #a=x  # load local 0 (x) into a scratch register.
    imulq  $5, %rbx   #a*=x     # multiply it by 5
    movl  %rbx, -40(%ebp) #y=a   # store the result in local 1 (y)

    movl  -20(%ebp), %eax   # move local 1 (y) into the result register

    #################### epilogue of function restores the stack

    popq %r15          # restore callee-saved registers
    popq %r14
    popq %r13
    popq %r12
    popq %rbx

    movq %rbp, %rsp    # reset stack to base pointer.
    popq %rbp          # restore the old base pointer

    ret                # return to caller

main 簡單代碼分析

//test2.cpp

long x=0;
long y=10;

int main()
{
    x = y;
    printf("value: %d",y);
    return 0;
}
    .file   "test2.cpp" #文件名 test2.cpp
    .globl  x   #全局變量 x
    .bss        #靜態存儲區域
    .align 8    #內存對齊,插入8位,一個字節
    .type   x, @object
    .size   x, 8 #x 8字節
x:
    .zero   8 
    .globl  y #設置全局變量  y
    .data   
    .align 8
    .type   y, @object
    .size   y, 8
y:
    .quad   10
    .section    .rodata
.LC0:
    .string "value: %d"
    .text
    .globl  main
    .type   main, @function
main:
.LFB2:
    .cfi_startproc   #主函數開始
    pushq   %rbp     
    .cfi_def_cfa_offset 16 #
    .cfi_offset 6, -16
    movq    %rsp, %rbp    #設置棧指針
    .cfi_def_cfa_register 6
    movq    y(%rip), %rax #將值10移動到%rax
    movq    %rax, x(%rip) #將%rax移動到棧中,並分配8個字節
    movq    y(%rip), %rax #將y值複製到%rax寄存器
    movq    %rax, %rsi #將參數壓入%rsi
    movl    $.LC0, %edi 
    movl    $0, %eax # 記錄浮點參數個數
    call    printf #使用printf函數
    movl    $0, %eax # 返回值0
    popq    %rbp  #指針出站
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

無優化彙編代碼分析

有了前面的基礎知識,讓我們再來看一下最開始的a.s

    .file   "a.c"  #指明源文件名
    .section    .rodata #定義靜態數據,只讀
.LC0: #定義標籤LC0
    .string "%d\n" #定義string 
    .text #定義代碼段
    .globl  main #定義全局標籤main
    .type   main, @function #定義標籤main 為一個函數
main:
.LFB2: #定義標籤LFB2
    .cfi_startproc #定義函數開始
    pushq   %rbp #棧的基底地址
    .cfi_def_cfa_offset 16 #CFA偏移16個字節
    .cfi_offset 6, -16 #寄存器先前值保存在CFA 16字節偏移處
    movq    %rsp, %rbp #棧頂指針指向棧底
    .cfi_def_cfa_register 6 #定義新的標準地址為6
    subq    $16, %rsp #棧指針向下分配16個字節
    movl    $10, -4(%rbp) #將10移動到棧底四個字節處
    movl    -4(%rbp), %eax #將10 複製到%eax-> int a=10
    imull   -4(%rbp), %eax #10*10-> a*=a; a=100;(%eax)=100;
    movl    %eax, -4(%rbp) #-4(%rbp)=100;
    movl    -4(%rbp), %eax # 複製-4(%rbp)的值到%eax;即(%eax)=100;
    subl    %eax, -4(%rbp) # -4(%rbp)=-4(%rbp)減(%eax);即100-100=0;-4(%rbp)=0;
    movl    -4(%rbp), %eax # 複製-4(%rbp)到%eax,即(%eax)=0
    addl    %eax, -4(%rbp) # -4(%rbp)=%eax+(-4(%rbp));即-4(%rbp)=0;
    movl    -4(%rbp), %eax # 將-4(%rbp)複製到%eax;%eax=0;
    movl    %eax, %esi     #%eax複製到%esi,結果存儲在%esi中
    movl    $.LC0, %edi   #載入LC0標籤,到%edi
    movl    $0, %eax      #將立即數0複製到%eax
    call    printf      #調用打印函數
    movl    $0, %eax   #複製0到%eax;return 0;
    leave #返回main函數
    .cfi_def_cfa 7, 8  #定義新的幀計算規則
    ret #結束當前函數
    .cfi_endproc #函數結束
.LFE2:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

從上面可以看出式a+=a-=a*=a;的執行順序是:

  • a*=a:a=100
  • a-=a:a=0;
  • a+=a:a=0;

分析開啓-O3優化後的代碼如下:

//a1.s

    .file   "a.c"
    .section    .rodata.str1.1,"aMS",@progbits,1 
.LC0:
    .string "%d\n"  #聲明 string 放在.text 區域 
    .section    .text.unlikely,"ax",@progbits 
.LCOLDB1:
    .section    .text.startup,"ax",@progbits
.LHOTB1:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB23:
    .cfi_startproc
    subq    $8, %rsp #設置棧頂指針值為8
    .cfi_def_cfa_offset 16 #設置框架偏移
    xorl    %edx, %edx #異或運算為0
    movl    $.LC0, %esi #將字符串參數,添加到%esi寄存器
    movl    $1, %edi #(%edi)寄存器賦值為1
    xorl    %eax, %eax #繼續進行異或運算,將結果參數放到eax寄存器
    call    __printf_chk #輸出函數
    xorl    %eax, %eax #異或運算為0
    addq    $8, %rsp #將8壓入%rsp
    .cfi_def_cfa_offset 8
    ret    #返回
    .cfi_endproc #函數結束
.LFE23:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE1:
    .section    .text.startup
.LHOTE1:
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

通過上面我們可以發現,編譯器直接進行了一次亦或運算a=(a!|a)=0;迷之編譯優化。

下面是上面計算式的java代碼

//a.java

public class a{
      public static void main(String[]agrs)
      {
          int a=10;
          a+=a-=a*=a;
            System.out.println("a value is:"+a);
      }

}

//編譯指令:javac a.java

//執行:java a

//結果:a value is:-80

可以看出同樣的式子,和C++計算結果完全不同
參考鏈接: Java反彙編及JVM指令集;查看Java的彙編指令

指令生成彙編代碼javap -c a.class

Compiled from "a.java"
public class a {
  public a();
    Code:
       0: aload_0 
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        10 //將10壓入棧
       2: istore_1 //將int類型值存入局部變量1
       3: iload_1  //讀取局部變量1 注意這裏讀取了四個局部變量,每個都為10
       4: iload_1   //讀取局部變量1
       5: iload_1   //讀取局部變量1
       6: iload_1   //讀取局部變量1
       7: imul    //執行乘法 10*10=100
       8: dup     //複製一個棧頂內容 100
       9: istore_1 //存入局部變量1 100
      10: isub    //執行減法-> 10-100=-90
      11: dup   //複製棧頂元素 -90
      12: istore_1 //存儲元素 -90
      13: iadd //加法: -90+10=-80;注意10一直在棧中沒有取出
      14: istore_1 //將-80存入局部變量1
      15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      18: new           #3                  // class java/lang/StringBuilder
      21: dup //複製棧頂內容 -80
      22: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V 
      25: ldc           #5                  // String a value is:
      27: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      30: iload_1  //加載局部變量1 -80
      31: invokevirtual #7                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      34: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      37: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: return
}

從上面彙編代碼可以看出;Java執行是直接將四個初始化臨時的變量,每個給予初始值10;依次進行*-+運算;每次的值變化為:100,-90,-80;因此最終結果是-80;所以c++和java的不同,是由於其內部的編譯器語法解析方式所決定的。也可以看出,Java真的比較耗性能。