第78部分- Linux x86 64位彙編 創建優化的代碼

第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 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章