第78部分- Linux x86 64位彙編 創建優化的代碼
我們可以用gcc編譯器從C程序創建編譯器優化後的彙編代碼,然後分析優化。
以tempconv.c文件爲例,將華氏溫度轉爲攝氏溫度。
#include <stdio.h>
float convert(int deg)
{
float result;
result = (deg - 32.) / 1.8;
return result;
}
int main()
{
int i = 0;
float result;
printf(" Temperature Conversion Chart\n");
printf("Fahrenheit Celsius\n");
for(i = 0; i < 230; i = i + 10)
{
result = convert(i);
printf(" %d %5.2f\n", i, result);
}
return 0;
}
彙編之後得到彙編代碼:
gcc -S tempconv.c
編譯器產生彙編代碼
彙編代碼如下,我們將其進行了註釋。:
.file "tempconv.c";//源文件名字
.text ;//定義代碼段,是隻讀和可執行的,後面那些指令都屬於.text段。
.globl convert ;//定義函數符號,.globl指示告訴彙編器,這個符號要被鏈接器用到,所以要在目標文件的符號表中標記它是一個全局符號
.type convert, @function ;//定義convert函數
convert:
.LFB0:
.cfi_startproc ;//函數開始符號
pushq %rbp;//壓棧rbp保存
.cfi_def_cfa_offset 16;// CFA(Canonical Frame Address),CFA定義爲調用站點上的前一幀的堆棧指針的值, CFA現在與當前堆棧指針的偏移量爲16個字節。CFI是CFI代表調用幀信息.
.cfi_offset 6, -16;//CFI指令用於調試。 它允許調試器展開堆棧。寄存器的先前值保存在與CFA偏移的位置。register 6
movq %rsp, %rbp;//複製當前rsp到rbp中
.cfi_def_cfa_register 6;// .cfi_def_cfa_register修改用於計算CFA的規則。將使用寄存器而不是舊的寄存器。 偏移量保持不變。register 6
movl %edi, -20(%rbp) ;//複製edi到rbp中,在堆棧下方空地處,這裏就是函數參數
cvtsi2sd -20(%rbp), %xmm0;//將參數,從 1個雙字有符號整數變成1個雙精度浮點數,到xmm0中。
movsd .LC0(%rip), %xmm1;//將雙字節32傳送到xmm1中。
subsd %xmm1, %xmm0
movsd .LC1(%rip), %xmm1;//將雙字節1.8傳送到xmm1中。
divsd %xmm1, %xmm0;//除以1.8
cvtsd2ss %xmm0, %xmm2;// 將雙精度轉換爲單精度浮點值
movss %xmm2, -4(%rbp);//結果移動到堆棧
movss -4(%rbp), %xmm0;//通過xmm0返回
popq %rbp
.cfi_def_cfa 7, 8
ret ;//函數返回
.cfi_endproc ;//函數結束符號
.LFE0:
.size convert, .-convert;//函數字節數量
.section .rodata;//只讀段
.align 8 ;// .align的作用在於對指令或者數據的存放地址進行對齊,有些CPU架構要求固定的指令長度並且存放地址相對於2的冪指數圓整,否則程序無法正常運行,如arm。.align的作用範圍只限於緊跟它的那條指令或者數據,而接下來的指令或者數據的地址由上一條指令的地址和其長度決定。
.LC2:
.string " Temperature Conversion Chart" ;//定義字符串
.LC3:
.string "Fahrenheit Celsius" ;//定義字符串
.LC4:
.string " %d %5.2f\n"
.text ;//定義代碼段,只讀並可執行
.globl main
.type main, @function ;//定義main函數
main:
.LFB1:
.cfi_startproc;//函數開始符號
pushq %rbp;//壓棧rbp保存
.cfi_def_cfa_offset 16;// CFA現在與當前堆棧指針的偏移量爲16個字節
.cfi_offset 6, -16;//6號寄存器是rbp
movq %rsp, %rbp;//複製當前rsp到rbp中
.cfi_def_cfa_register 6
subq $16, %rsp;//騰出堆棧空地,2格。
movl $0, -8(%rbp) ;//複製本地變量0到堆棧中。
leaq .LC2(%rip), %rdi;//字符串地址賦值給rdi,調用輸出函數puts,源代碼中的printf
call puts@PLT
leaq .LC3(%rip), %rdi;// 字符串地址賦值給rdi,調用輸出函數puts, 源代碼中的printf
call puts@PLT
movl $0, -8(%rbp) ;//複製本地變量i=0到堆棧中
jmp .L4
.L5:
movl -8(%rbp), %eax;//複製本地變量i到eax
movl %eax, %edi;//複製本地變量i到edi
call convert;//調用函數convert。
movd %xmm0, %eax;//從xmm0獲取結果
movl %eax, -4(%rbp)
cvtss2sd -4(%rbp), %xmm0;// 將單精度轉換爲雙精度浮點值
movl -8(%rbp), %eax;//華氏攝氏度,取出->eax->esi,調用printf
movl %eax, %esi;//華氏設置度整型放於esi.
leaq .LC4(%rip), %rdi;//字符串地址。
movl $1, %eax
call printf@PLT;//調用printf函數
addl $10, -8(%rbp) ;//調用增加10給變量i,即是for中i+10
.L4:
cmpl $229, -8(%rbp);//對比229和-8(%rbp)的變量,就是for循環中的比較
jle .L5;//小於等於就調整到.L5,否則就退出程序。
movl $0, %eax;//移動0到eax,函數返回。
leave;// Leave的作用相當mov esp,ebp和pop ebp。
.cfi_def_cfa 7, 8;//7號寄存器是rsp。實現rsp+8。
ret
.cfi_endproc;//函數開始符號
.LFE1:
.size main, .-main;//該函數的函數字節數
.section .rodata;//只讀段,不可執行
.align 8
.LC0:;//保存32浮點。
.long 0
.long 1077936128
.align 8
.LC1: ;//保存1.8浮點。
.long 3435973837
.long 1073532108
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0";//GCC註釋,連接器會移除。
.section .note.GNU-stack,"",@progbits
這裏的.section是彙編指示(Assembler Directive)或叫做僞操作(Pseudo-operation)
.section指示把代碼劃分成若干個段(Section),程序被操作系統加載執行時,每個段被加載到不同的地址,操作系統對不同的頁面設置不同的讀、寫、執行權限。
這裏的CFA描述如下:
: :
| whatever | <--- CFA
+----------------+
| return address |
+----------------+
| reserved space | <--- %rsp == CFA - 16
+----------------+
使用.cfi_def_cfa_offset指令在調試信息中聲明瞭堆棧指針的更改,並且可以看到CFA現在與當前堆棧指針的偏移量爲16個字節。
優化後代碼分析
註釋結束後,我們看下優化點分析。
#gcc -S -O3 tempconv3.s tempconv.c
.file "tempconv.c"
.text
.p2align 4,,15
.globl convert
.type convert, @function
convert:
.LFB23:
.cfi_startproc
pxor %xmm0, %xmm0;//異或清零
cvtsi2sd %edi, %xmm0;//是參數,華氏攝氏度。
subsd .LC0(%rip), %xmm0;//直接減去LC0中的38
divsd .LC1(%rip), %xmm0;//直接除去LC1中的1.8。
cvtsd2ss %xmm0, %xmm0;//轉化爲單精度。
ret
.cfi_endproc
.LFE23:
.size convert, .-convert
.section .rodata.str1.8,"aMS",@progbits,1
.align 8
.LC2:
.string " Temperature Conversion Chart"
.section .rodata.str1.1,"aMS",@progbits,1
.LC3:
.string "Fahrenheit Celsius"
.LC4:
.string " %d %5.2f\n"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB24:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
pushq %rbx;//保存rbx寄存器
.cfi_def_cfa_offset 24
.cfi_offset 3, -24
leaq .LC2(%rip), %rdi
leaq .LC4(%rip), %rbp
xorl %ebx, %ebx;//ebx清零
subq $8, %rsp
.cfi_def_cfa_offset 32
call puts@PLT
leaq .LC3(%rip), %rdi
call puts@PLT
.p2align 4,,10
.p2align 3
.L4:
pxor %xmm0, %xmm0
movl %ebx, %edx
movq %rbp, %rsi
movl $1, %edi
movl $1, %eax
cvtsi2sd %ebx, %xmm0
addl $10, %ebx
subsd .LC0(%rip), %xmm0
divsd .LC1(%rip), %xmm0
cvtsd2ss %xmm0, %xmm0
cvtss2sd %xmm0, %xmm0
call __printf_chk@PLT
cmpl $230, %ebx
jne .L4
addq $8, %rsp
.cfi_def_cfa_offset 24
xorl %eax, %eax
popq %rbx
.cfi_def_cfa_offset 16
popq %rbp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE24:
.size main, .-main
.section .rodata.cst8,"aM",@progbits,8
.align 8
.LC0:
.long 0
.long 1077936128
.align 8
.LC1:
.long 3435973837
.long 1073532108
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
可以通過如下直接彙編執行
#as -o tempconv3.o tempconv3.s
#gcc -o tempconv3 tempconv3.o
我們發現main中其實沒有調用convert函數了,被優化掉了,功能被直接嵌入到main函數中了。
而convert函數本身也是被大量精簡化了。
參考
這裏由於細節比較多,加入了一個參考鏈接,關於cfi的描述。
https://sourceware.org/binutils/docs-2.17/as/CFI-directives.html#CFI-directives
CFI directives in assembly files