我们可以用gcc编译器从C程序创建编译器优化后的汇编代码,然后分析优化。
以tempconv.c文件为例,将华氏温度转为摄氏温度。
#include
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