avatar

6.5日更新 (week 7)

准备在医院看的一点书的内容进行一下整理,进行小黄书更新,能今天修改了学习计划,觉得先把CTF wiki都去看一下,这样入门比较快一点,然后每天还是之后开始学习正向,然后再学逆向

小黄书更新

表达式短路

表达式短路时通过逻辑与运算和逻辑或运算使语句根据条件在执行的时候发生中断,看一下汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//C++源码说明:递归函数,用于计算整数累计,nNumber为累加值
int Accumulation(int nNumber){
//当nNumber等于0时,逻辑与运算符左边的值为假,不会执行右边的语句
//形成表达式短路,从而找到递归出口
nNumber && (nNumber += Accumulation(nNumber - 1));
return nNumber
}
//对应汇编
int Accumulation(int nNumber){
nNumber && (nNumber += Accumulation(nNumber - 1));
//这里为短路模式汇编代码,比较变量nNumber是否等于0
0040BAA8 cmp dword ptr [ebp+8],0
//通过JE跳转,检查ZF标记位等于1跳转
0040BAAC je Accumulation+35h (0040bac5)
//跳转失败,进入递归调用
0040BAAE mov eax,dword ptr [ebp+8]
//对变量nNumber减1后,结果作为参数压栈
0040BAB1 sub eax,1
0040BAB4 push eax
//继续调用自己,形成递归
0040BAB5 call @ILT+30(Accumulation) (00401023)
0040BABA add esp,4
0040BABD mov ecx,dword ptr [ebp+8]
0040BAC0 add ecx,eax
0040BAC2 mov dword ptr [ebp+8],ecx
//返回变量nNumber
return nNumber
0040BAC5 mov eax,dword ptr [ebp+8]
}

测试一下

image-20200605085223132

image-20200605085446570

所以通过递归函数Accumulation进行了整数的累加和计算,之后用&&的原则,左边如果为0就输出出去,用这个原则来决定跳转的过程,逻辑运算 || 与 && 有些不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//c++源码说明,和上面的类似
int Accumulation (int nNumber){
//当nNumber等于0时,逻辑或运算符的左边如果为真就不执行右边的语句
//形成表达式短路,从而找到递归的出口
(nNumber == 0) || (nNumber += Accumulation(nNumber - 1));
return nNumber;
}
//C++源码与对应汇编代码
int Accumulation(int nNumber){
(nNumber == 0) || (nNumber += Accumulation(nNumber - 1));
00401618 cmp dword ptr [ebp+8],0
0040161C je Acumulation+35h (00401635)
0040161E mov eax,dword ptr [ebp+8]
00401621 sub eax,1
00401624 push eax
00401625 call @ILT+30(Accumulation) (00401023)
0040162A add esp,4
0040162D mov ecx,dword ptr [ebp+8]
00401630 add ecx,eax
00401632 mov dword ptr [ebp+8],ecx
return nNumber
00401635 mov eax,dword ptr [ebp+8]
}

条件表达式

条件表达式就是我们说的三目运算,也就是 表达式1 ?表达式2 :表达式3

表达式2 ,3为常量的时候,条件表达式可以被优化,而表达式2或者表达式3中的一个为变量的时候,条件表达式不可以被优化,会转换成分支结构,而表达式1为一个常量值,编译器会在编译期间得到答案,就不会有条件表达式存在

  • 表达式1为简单比较,而表达式2,3的差值等于1
  • 表达式1为简单比较,而表达式2,3的差值大于1
  • 表达式1为复杂比较,而表达式2,3的差值大于1
  • 表达式2,3有一个为变量,于是没有优化

转换方案1

1
2
3
4
5
6
7
8
9
10
11
12
13
//c++源码说明,条件表达式
int Condition(int argc , int n){
//比较参数argc是否等于5,真值返回5,假值返回6
return argc == 5 ? 5 : 6;
}
//汇编比较
//清空eax
00401678 xor eax,eax
0040167A cmp dword ptr [ebp+8],5
//setne检查ZF标记位,当ZF==1,则赋值al为0,反之则赋值al为1
0040167E setne al
//若argc等于5则al==0,反之al==1,执行这句后,eax正好为5/6
00401681 add eax,5

测试的时候

image-20200605104331427

大概读了一下,这个用了两个mov进行付给eax,之后跳转到各自的位置上,进行一个赋值的

转换方案2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//c++源码说明,条件表达式
int Condition(int argc , int n){
//比较参数argc是否等于5,真值返回5,假值返回6
return argc == 5 ? 4 : 10;
}
//汇编比较
return argc == 5 ? 4 : 10;
00401678 mov eax,dword ptr [ebp+8]
0040167B sub eax,5
0040167E neg eax
00401680 sbb eax,eax
//eax的取值只可能为0或者0xFFFFFFFF
00401682 and eax,6
00401685 add eax,4

对于argc == 5这种等职的比较,VC++会使用减法和求补运算来判断是否为真值,如果我们的argc不为5,那么执行sub指令后的eax就不为0,neg的指令,会将eax的符号位发生改变,也就是求补的运算,如果eax为0,也就是argc为5的话,那么0进行补+1,也是0,那么CF=0,我们现在假设CF=1的情况,那么执行sbb eax,eax 等同于了 eax-eax-CF 那么eax会变成0xFFFFFFFF,另一个情况就是0,使用eax与6进行与运算,如果eax数值为0xFFFFFFFF那么 想与就是6,相加4就是10,如果是0的情况那么直接就是0+4 = 4

1
2
3
4
5
6
7
总结:
sub reg,A
neg reg
sbb reg,reg
and reg,B
add reg,C
//reg == A ? C : B

转换方案3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//C++源码说明,条件表达式
int Condition(int argc,int n){
return argc < = 8 ? 4 :10;
}
//汇编代码
return argc < = 8 ? 4 :10;
//清空eax,与方案1类似
00401678 xor eax,eax
0040167A cmp dword ptr [ebp+8],8
//根据变量与8进行比较的结果,使用setg指令,当标记位SF=OF且ZF=0赋值al为1
符号标志位SF(Sign Flag),符号标志SF用来反映运算结果的符号位,他与运算结果的最高位相同
溢出标志位OF(Overflow Flag):比如有一个杯子,放水放满了再放就出去了,叫溢出,但是怎么区别看起来都是最高位的问题
零标志位ZF(Zero Flag),零标志ZF用来反映运算结果是否为0,如果运算结果为0,则值为1,否则值为0,在判断运算结果是否为0时,可以用此标志位
//用于检查变量数据是否大于8,大于则赋值1,小于就赋值0
0040167E setg al
//此时al中只能为0或1,执行自减的操作,eax中为0xFFFFFFFF或0
00401681 dec eax
//使用al和0xFA做与运算,eax中是0xFFFFFFFA或者0
//2-3=0xFFFFFFFA
00401682 and al,0FAh
//由于eax只能有两个结果0xFFFFFFFA(-6)或0,加0x0A后结果比然为4,10
00401684 add eax,0Ah

无忧化使用分支结果

如果表达式2或者表达式3中的值为未知数时候,就无法使用之前的方案去优化,编译器会那招正常的语句流程进行比较和判断,选择对应的表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//C++源码说明:条件表达式
int Condition(int argc,int n){
return argc ? 8 : n;
}
//汇编代码
return argc ? 8 : n;
//比较变量argc
00401448 cmp dword ptr [ebp+8],0
//使用JE跳转,检查变量argc是否等于0,跳转的地址为0x00401457位置
004014CC je Condition+27 (00401457)
//跳转失败说明操作数1为真,将表达式1的值(立即数8)存入局部变量ebp-4中
0040144E mov dword ptr [ebp-4],8
//跳转到返回值赋值处
00401455 jmp Condition+2Dh (00145d)
//参数2的数据存入eax中
00401457 mov eax,dword ptr [ebp+0Ch]
0040145A mov dword ptr [ebp-4],eax
0040145D mov eax,dword ptr [ebp-4]
//恢复现场
00401466 ret

如果经过O2选项优化的Release版中,这些代码都会被编译为分支结构

位运算

<<:左移运算,做高位左移到CF位置,最低位补0

>>:右移运算,最高位不变,最低位右移到CF中

|:位或运算,两个数的相同位上,只要有一个为1,则结果为1

&:位与运算,在两个数的相同位上,只要同时为1时,结果才为1

^:异或运算,在两个数的相同位上,当两个值相同时为0,不同时为1

~:取反运算,将操作数每一位上的1变0,0变1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//C++源码对应汇编,位运算(有符号数)
//左移运算3次
argc = argc << 3;
00401498 mov eax,dword ptr [ebp+8]
//左移运算对应汇编指令SHL
0040149B shl eax,3
0040149E mov dword ptr [ebp+8],eax
//C++源码对比,右移运算5
argc = argc >> 5;
004014A1 mov ecx,dword ptr [ebp+8]
//右移运算对应汇编指令SAR
004014A4 sar ecx,5
004014A7 mov dword ptr [ebp+8],ecx
//C++源码对比,位或运算,变量argc低16位不变,高16位设置为1
argc = argc | 0xFFFF0000
004014AA mov edx,dword ptr [ebp+8]
//位或运算对应汇编指令OR
004014AD or edx,0FFFF0000h
004014B3 mov dword ptr [ebp+8],edx
//C++源码对比,将变量argc低16位清0,高位不变
argc = argc & 0xFFFF0000
//位与运算对应汇编指令AND
004014B9 and eax,0xFFFFh
004014BE mov dword ptr [ebp+8],eax
//C++源码对比,对变量argc做异或运算
argc = argc ^ 0xFFFF0000
004014C1 mov ecx,dword ptr [ebp+8]
//异或运算对应汇编指令XOR
004014C4 xor ecx,0FFFF0000h
004014CA mov dword ptr [ebp+8],ecx
//C++源码对比,将argc按位取反
argc = ~argc;
004014CD mov edx,dword ptr [ebp+8]
//取反运算对应汇编指令NOT
004014D0 not edx
004014D2 mov dword ptr [ebp+8],edx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//C++源码说明:无符号数位移
int BitOperation(int argc){
unsigned int nVar = argc;
nVar <<= 3;
nVar >>= 5;
}
//C++源码对应汇编代码讲解
unsigned int nVar = argc;
004016C8 mov eax,dword ptr [ebp+8]
004016CB mov dword ptr [ebp-4],eax
//C++源码对比,对变量nVar左移3位
nVar <<= 3;
004016CE mov ecx,dword ptr [ebp-4]
//和有符号数左移一样
004016D1 shl ecx,3
004016D4 mov dword ptr [ebp-4],ecx
//C++源码对比,对变量nVar右移5位
004016D7 mov edx,dword ptr [ebp-4]
//使用shr进行右移位,最高位补0,最低位进CF
004016DA shr edx,5
004016DD mov dword ptr [ebp-4],edx

左移的时候,有符号数和无符号数(unsigned)的移位操作是一样的,都不需要考虑符号位,但是右移的时候,有符号进行的是sar,无符号进行的是shr,所以无符号的时候不需要符号位,直接使用shr将最高位补0即可

编译器使用的优化技巧

讨论一下基于Pentiun微处理器的优化技术

代码优化一般有四个方向:

  • 执行速度优化
  • 内存存储空间优化
  • 磁盘存储空间优化
  • 编译时间优化

常量折叠

x = 1 + 2

1和2都是常量,结果可以直接知道,那么就是3,所以没有必要产生add的指令,直接生成x = 3即可

常量传播

如果上面的代码接着下面有一个 y = x + 3,由于上面最后生成了x = 3,那么结果还是可以遇见的,所以直接生成 y = 6即可

减少变量

假设一个 x = i*2 y = j*2 if(x>y){}这里的x和y比较等价于i和j的比较,所以如果后面没有引用x,y,那么就会直接去掉x,y,生成 if(i>j),那么假设 x = i*2 y = i*2所以 i*2 叫做公共表达式,可以归并为一个,x = i*2 y = x

复写传播

类似于常量传播,但是目标变成了变量

x = a; ...... y = x+c ;

如果中间的代码中没有修改变量x,那么可以直接用变量a代替x

y = a + c

减去不可以达分支(剪支优化)

if ( 1 > 2 ) 如果代码用换不可能被执行,那么整个if代码块就会有不存在的理由

等等的方案

目标代码生成阶段的优化方案

  • 流水线优化
  • 分支优化
  • 高速缓存 ( cache ) 优化

流水线优化规则

指令工作流程

  1. 取指令:CPU从高速缓存或内存中取机器码
  2. 指令译码:分析指令的长度,功能和寻址方式
  3. 按寻址方式确定操作数:指令的操作数可以是寄存器,内存单元或者立即数,如果操作数在内存单元里,这一步就要计算出有效地址
  4. 取操作数:按操作数存放的位置获得数值,并存放在临时寄存器中
  5. 执行指令:由控制单元或者计算单元执行指令规则的操作
  6. 存放计算结果

假设

1
2
执行指令: add	eax,dword ptr ds:[ebx+40DA44]
对应的机器代码: 0383 44DA4000

Intel处理器位小端序排列,数据的高位对应内存的高地址,低位对应内存的低地址

步骤:取指令,得到第一个十六进制字节:0x03,并且eip+1,译码知道这个指令是加法,但是信息不够,于是乎将0x03放入处理器的指令队列缓存中,取指令得到第二个十六进制字节:0x83,机器码放入处理器的指令队列缓存中,eip+1,译码后知道这个寄存器相对寻址方法的加法,而且参与寻址的寄存器是ebx,存放的目标是eax,后面还有4字节的偏移,指令长度确定后,机器码放入处理器的指令队列,取地址,得到第三个十六进制字节:0x44,这是指令中包含的4字节地址偏移量信息的第一个字节,放入内部暂存区,ebx保存在ALU,准备计算有效的地址,eip+1,之后依次开始取指令0xDA 0x40 0x00 放入寄存器,eip依次+1,这时候eax的值传给ALU,调度MMU,得到内存单元,传送到ALU,计算结果,最后将计算结果存回eax中

流水线

由于每条指令的工作流程都是由取指令,译码,执行,回写的步骤组成,所以很多处理器设计了多流程的结构,A流水线处理过程中,B流水线可以提前对下一条指令做处理

1
2
004010AA	mov	eax,92492493h
004010AF add esp,8

我们的处理器会先读取004010AA处的二进制指令,之后开始进行译码等操作,这些工作的每一个步骤都是需要时间的,如果我们取指令,内存管理单元开始工作,其他的部件进行闲置等待,等拿到了指令才进行下一步的工作,于是我们为了提高效率,开始了流水线的这一个机制

因为流水线的机制,我们在执行mov eax,92492493h过程中对第二条的地址进行读取以及译码,进行并行处理,那么我们提高了处理器的工作的效率

指令相关性

如果后一条指令的执行依赖前一条指令的硬件资源,那么这样的情况就是指令相关

1
2
add	edx,esi
sar edx,2

两条指令都需要访问并设置edx,所以只能执行完add edx,esi后才能执行 sar edx,2,这样的情况产生了寄存器的争用,影响了效率

地址相关性

这个和上一个指令相关差不多,这个等待的是内存地址的争用

1
2
add	[00401234],esi
mov eax,[00401234]

由于第一条指令访问的是0x401234地址,那么只能第一条指令操作完后再去执行第二条语句,会影响效率,VC++的O2的release选项生成的代码会考虑流水线执行的工作方式

1
2
3
4
5
6
7
8
9
10
11
0040101F	push eax
00401020 push offset aNvarone2D ; "nVarOne / 2 = %d"
00401025 call _printf
0040102A mov eax,92492493h
0040102F add esp,8
00401032 imul esi
00401034 add edx,esi
00401036 sar edx,2
00401039 mov eax,edx
0040103B shr eax,1Fh
0040103E add edx,eax

恢复栈顶的指令add esp,8,中间有mov eax,92492493h指令,这里为流水线优化,因为后面的imul esi需要设置eax,吧计算结果的低位放在eax中,那么中间换成add esp,8防止了寄存器争用,后面的这里的 add edx,esi 与 sar edx,2不能移动是因为,在后面的mov eax,edx 与 edx有关系,如果位置移动的话,我们会出现edx的一个的计算结果不正确,于是乎不能改变顺序

明天继续看!为了爱与和平!睡觉!😴

Author: L0x1c
Link: https://l0x1c.github.io/2020/06/04/2020-6-04/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶