问题
一段c++反汇编代码阅读的问题
编译器是gcc,高级语言代码如图
#include <stdio.h>
int main(int argc, char* argv[])
{
if (argc == 0) {
argc = 5;
} else
{
argc = 6;
}
printf("%d\n", argc);
getchar();
return 0;
}
dbg反汇编中对应main函数结果如下
push rbx
sub rsp, 20
mov ebx, ecx
call a.7FF74BC515D0
cmp ebx, 1
mov edx, 5
lea rcx, qword ptr ds:[7FF74BC59000] ;
sbb edx, FFFFFFFF
call a.7FF74BC57320
call <JMP.&_fgetchar>
xor eax, eax,
add rsp, 20
pop rbx
ret
看不懂if语句为什么优化成这个样子了,谁能帮忙解读一下,最好每句汇编都能解读一下,非常感谢!
答疑
不太清楚题主的目的是为了了解编译器优化问题,还是为了逆向工程,把汇编代码反编译成c/c++等高级语言
题主需要清楚的一点是,现代的编译器是非常复杂的,也是先进的,我们平常安装的软件都经过编译器的优化,想从程序反汇编代码直接的、精确的看到源代码的一些结构,比如if/else for/while switch/case等,很多时候是不太可能的,经常的情况是我们知道这里有一个循环或者跳转,但是源代码里究竟是一个什么结构,需要我们仔细分析。分析过程中可以借助很多反编译器,比如IDA hexray decompiler等,反编译器会帮助我们得到一个大致的源代码,如果源代码的函数比较简单,反编译结果会和源代码的结构基本类似,如果源代码某个函数很复杂,有很多跳转/循环,反编译器也经常出错,只能看到一个大概,里面会有很多goto语句,需要仔细分析才能掌握源代码的流程
下面我从编译器优化的角度分析一下这个问题
首先,这个if-else的语义还是存在的,不管编译器如何优化,如果语义变了,那么它优化后生成的程序就是错误的,不符合程序开发者的预期,这是编译器开发者绝对不允许的。只是形式上与我们认为的if-else结构不太一样
其次,不同的编译器,不同版本的编译器(比如gcc),最后生成的代码可能不一样,比如在我的机器上,gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1),就不会生成和作者一样的代码,下面的代码可能和题主自己执行的效果不一样,但是原理是一样的,自己实践一下应该就会理解
想要了解为什么会生成这样的代码,
首先需要了解编译过程,编译过程分为很多阶段,简单的来说,以gcc为例,经过词法 语法分析之后,gcc会把源代码转换成一种中间语言的代码,gcc目前使用的是gimple这种中间语言。
源代码转换成gimple语言的代码之后,gcc会对生成的gimple代码经过一遍又一遍的处理(这种处理一般被称为pass),里面有很多做优化的pass/处理(也有其他的pass)(优化的技术有很多种,可以单独成书了,不具体说了),gimple代码经过优化之后,才会传递给编译器的后端,后端负责将gimple转换成机器代码(也就是经过反汇编看到的代码),其中后端也有不少优化技术,经过多轮优化之后,才会生成最终我们看到的代码
为了详细了解这个过程,gcc中可以使用-fdump-tree-gimple选项生成对应的GIMPLE文件,也可以使用-fdump-tree-all或-fdump-tree-all-raw命令生成中间操作的所有文件
在我的机器上运行 gcc -O2 -fdump-tree-all-raw test.cpp (test.cpp是题主的那个程序)会生成上百个中间文件
#include <stdio.h>
int main(int argc, char* argv[])
{
if (argc == 0) {
argc = 5;
} else
{
argc = 6;
}
printf("%d\n", argc);
getchar();
return 0;
}
再补充一个基本知识,编译器很多时候是以一个函数/方法为单位进行优化的(跨函数的优化也是有的),函数又被分成很多基本块 basic block,一个basic block,简单的说就是一个没有任何跳转、只能顺序执行的代码块,基本块只有一个执行入口 一个执行出口,可以包含N条语句(N>1),含跳转/循环等结构把函数分为一个个的basic block
对于test.cpp程序,一开始转换成的gimple语言代码是这样的,可以看到基本与源代码类似,if/else结果很清晰
int main(int, char**) (int argc, char * * argv)
{
int D.3051;
if (argc == 0) goto <D.3048>; else goto <D.3049>;
<D.3048>:
argc = 5;
goto <D.3050>;
<D.3049>:
argc = 6;
<D.3050>:
printf ("%d\n", argc);
getchar ();
D.3051 = 0;
return D.3051;
D.3051 = 0;
return D.3051;
}
int main(int, char**) (int argc, char * * argv)
gimple_bind <
int D.3051;
gimple_cond <eq_expr, argc, 0, <D.3048>, <D.3049>>
gimple_label <<D.3048>>
gimple_assign <integer_cst, argc, 5, NULL, NULL>
gimple_goto <<D.3050>>
gimple_label <<D.3049>>
gimple_assign <integer_cst, argc, 6, NULL, NULL>
gimple_label <<D.3050>>
gimple_call <printf, NULL, "%d\n", argc>
gimple_call <getchar, NULL>
gimple_assign <integer_cst, D.3051, 0, NULL, NULL>
gimple_return <D.3051 NULL>
gimple_assign <integer_cst, D.3051, 0, NULL, NULL>
gimple_return <D.3051 NULL>
>
第一句gimple_cond 是判断argc是否等于0,然后通过gimple_assign 直接赋值argc=5,或者跳转再赋值argc=6
经过多轮优化之后,gimple代码被优化成这样的
Removing basic block 3
int main(int, char**) (int argc, char * * argv)
{
struct _IO_FILE * stdin.0_4;
<bb 2> [100.00%]:
gimple_cond <eq_expr, argc_2(D), 0, NULL, NULL>
goto <bb 4>; [46.00%]
else
goto <bb 3>; [54.00%]
<bb 3> [54.00%]:
<bb 4> [100.00%]:
# gimple_phi <argc_1, 5(2), 6(3)>
gimple_call <__printf_chk, NULL, 1, "%d\n", argc_1>
gimple_assign <var_decl, stdin.0_4, stdin, NULL, NULL>
gimple_call <_IO_getc, NULL, stdin.0_4>
gimple_return <0 NULL>
}
可以看到函数已经分成很多basic block,也就是代码里的<bb 2>、<bb 3>、<bb 4>,并算出了各自概率
<bb 3>是空的(被优化掉了,可以看到Removing basic block 3),没有代码,所以gimple_cond 之后无论怎么跳转,都会跳转<bb 4>,
也就是说,那两个goto 和<bb 3>都是没用的,都不会在最终生成的代码里
那么为什么<bb 3>会是空的/被优化呢?
可以发现多了一条语句 # gimple_phi <argc_1, 5(2), 6(3)>
关于 gimple_phi 可以参考注释,可以简单将其理解为一个条件赋值语句(其实是一个语法树节点),就是根据前面的gimple_cond语句赋值5或者6
/* GIMPLE_PHI <RESULT, ARG1, ..., ARGN> represents the PHI node
RESULT = PHI <ARG1, ..., ARGN>
RESULT is the SSA name created by this PHI node.
ARG1 ... ARGN are the arguments to the PHI node. N must be
exactly the same as the number of incoming edges to the basic block
holding the PHI node. Every argument is either an SSA name or a
tree node of class tcc_constant. */
DEFGSCODE(GIMPLE_PHI, "gimple_phi", GSS_PHI)
为什么可以直接使用 gimple_cond + gimple_phi 来代替原来的gimple_cond + goto(jmp等指令)呢?
简单的说就是为了优化,现在CPU大部分支持一些条件赋值/操作类(不是条件跳转类)的指令,为什么呢?
因为现代CPU一般会流水线等的优化措施,大学课本里都有介绍CPU执行指令分为取指 译码 执行 访存等等微步骤,流水线可以并行执行这些微步骤从而同时执行几条指令,但是条件跳转会使CPU这种优化措施大打折扣,因为遇到跳转,可能跳转之后的语句并不是实际要执行的语句,CPU必须返工,造成性能损耗
所以,从CPU执行优化的角度来说,条件跳转指令是非常不好的,最好使用条件赋值/操作类(不是条件跳转类)的指令来替代
GCC等编译器的开发者知道这一点,所以生成指令时也会优先使用条件赋值/操作类(不是条件跳转类)的指令来替代条件跳转类指令
为此,我们的源代码有时候会被改的面目全非
回到上面,我们已经理解gimple_cond + gimple_phi 代替原来的gimple_cond + goto(jmp等指令)的原因,这两条gimple语句到了编译器后端生成机器指令时,就会优先选取一些条件赋值/操作类的机器指令
比如题主的环境中,if-else跳转实际上会被生成下面的指令
cmp ebx, 1
mov edx, 5
sbb edx, FFFFFFFF
这里的sbb就是一个条件赋值/操作类指令,用来替代条件跳转,以利于CPU流水线并行指令
如果ebx/argc=0,"cmp ebx,1 "造成CF置位为1,"sbb edx, FFFFFFFF"就是5-(-1)-1=5
如果ebx/argc!=0,"cmp ebx,1 "不影响CF,CF为0,"sbb edx, FFFFFFFF"就是5-(-1)-=6
上面说过了,不同的编译器或者不同版本的编译器,可能生成不同的指令,我的机器上(,gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1))生成的指令是这样的
.text:00000000000005E0 sub rsp, 8
.text:00000000000005E4 xor edx, edx
.text:00000000000005E6 test edi, edi
.text:00000000000005E8 setnz dl
.text:00000000000005EB lea rsi, unk_7B4
.text:00000000000005F2 mov edi, 1
.text:00000000000005F7 add edx, 5
.text:00000000000005FA xor eax, eax
.text:00000000000005FC call ___printf_chk
.text:0000000000000601 mov rdi, cs:__bss_start ; fp
.text:0000000000000608 call __IO_getc
.text:000000000000060D xor eax, eax
.text:000000000000060F add rsp, 8
.text:0000000000000613 retn
那个if-else循环变成了
.text:00000000000005E6 test edi, edi
.text:00000000000005E8 setnz dl
.text:00000000000005F2 mov edi, 1
.text:00000000000005F7 add edx, 5
道理是类似的,唯一不变的语义,也就是说与源代码中if-else语句的效果是一样的。
链接
https://ask.csdn.net/questions/7644124?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164498124016781685372856%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fask.%2522%257D&request_id=164498124016781685372856&biz_id=4&utm_medium=distribute.pc_search_result.none-task-ask_topic-2askfirst_rank_ecpm_v1~ask_rank-1-7644124.pc_ask&utm_term=%E6%B1%87%E7%BC%96&spm=1018.2226.3001.4187