[答疑] 一段c++反汇编代码阅读的问题

问题

一段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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
由于学生教师在线答疑系统是一个比较大的项目,所以在这里无法提供完整的代码。不过,以下是一个简单的在线答疑系统的示例代码,供参考: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> struct Question { char content[1000]; char answer[1000]; }; struct User { char username[50]; char password[50]; int is_teacher; }; void ask_question(struct Question* questions, int* num_questions, char* username) { char content[1000]; printf("请输入你的问题:"); getchar(); // 消耗掉上一次输入留下的回车符 fgets(content, 1000, stdin); content[strlen(content)-1] = '\0'; strcpy(questions[*num_questions].content, content); printf("问题已提交,等待回答!\n"); (*num_questions)++; } void answer_question(struct Question* questions, int num_questions, char* username) { int i; for (i = 0; i < num_questions; i++) { if (strlen(questions[i].answer) == 0) { printf("问题:%s\n", questions[i].content); char answer[1000]; printf("请输入回答:"); getchar(); // 消耗掉上一次输入留下的回车符 fgets(answer, 1000, stdin); answer[strlen(answer)-1] = '\0'; strcpy(questions[i].answer, answer); printf("回答已提交!\n"); return; } } printf("没有需要回答的问题!\n"); } void list_questions(struct Question* questions, int num_questions, char* username) { int i; for (i = 0; i < num_questions; i++) { if (strlen(questions[i].answer) == 0) { printf("问题:%s\n", questions[i].content); } else { printf("问题:%s\n回答:%s\n", questions[i].content, questions[i].answer); } } } int login(struct User* users, int num_users, char* username, char* password) { int i; for (i = 0; i < num_users; i++) { if (strcmp(users[i].username, username) == 0 && strcmp(users[i].password, password) == 0) { return users[i].is_teacher; } } return -1; } void register_user(struct User* users, int* num_users) { char username[50]; char password[50]; int is_teacher; printf("请输入用户名:"); scanf("%s", username); printf("请输入密码:"); scanf("%s", password); printf("请选择用户类型(1为教师,0为学生):"); scanf("%d", &is_teacher); strcpy(users[*num_users].username, username); strcpy(users[*num_users].password, password); users[*num_users].is_teacher = is_teacher; (*num_users)++; printf("用户注册成功!\n"); } int main() { struct Question questions[100]; int num_questions = 0; struct User users[100]; int num_users = 0; while (1) { int choice; printf("请选择操作:\n"); printf("1. 登录\n"); printf("2. 注册\n"); printf("3. 提问\n"); printf("4. 回答问题\n"); printf("5. 查看问题列表\n"); printf("6. 退出\n"); scanf("%d", &choice); if (choice == 1) { char username[50]; char password[50]; printf("请输入用户名:"); scanf("%s", username); printf("请输入密码:"); scanf("%s", password); int is_teacher = login(users, num_users, username, password); if (is_teacher == -1) { printf("登录失败!\n"); } else if (is_teacher == 1) { printf("教师登录成功!\n"); } else { printf("学生登录成功!\n"); } } else if (choice == 2) { register_user(users, &num_users); } else if (choice == 3) { ask_question(questions, &num_questions, users[num_users-1].username); } else if (choice == 4) { if (login(users, num_users, users[num_users-1].username, users[num_users-1].password) != 1) { printf("权限不足!\n"); } else { answer_question(questions, num_questions, users[num_users-1].username); } } else if (choice == 5) { list_questions(questions, num_questions, users[num_users-1].username); } else if (choice == 6) { printf("谢谢使用,再见!\n"); break; } else { printf("无效的选择,请重新选择!\n"); } } return 0; } ``` 这个示例代码只是一个简单的在线答疑系统,没有涉及到数据库、网络通信等高级技术,仅供参考。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

quentin_d

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值