Deutsh
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

#include <stdio.h>
int main()
{
int nIndex = 1;
scanf("%d",&nIndex);
switch(nIndex)
{
case 1:
printf("nIndex == 1");
break;
case 2:
printf("nIndex == 2");
break;
case 3:
printf("nIndex == 3");
break;
case 4:
printf("nIndex == 4");
break;
case 5:
printf("nIndex == 5");
break;
}
}
经测试,在分支超过4
条时在一定分支范围内,GCC
会采用跳表这种优化的方式来处理swicth
,代替低于4
条时,与if
一样的分支结构 ,从而加速其访问速度
但GCC
对于跳表的处理与VC6.0
的处理方式有所不同,先来说GCC
的处理方式
动态调试以上代码:
从scanf
输入后开始调试:
由于我们的case
是从1...5
依次增加的,所以从图中516c
处开始的3
条语句负责判断当前的输入是否超过了swicth
分支语句中case
的最大值
此处我们输入的值为2
,经scanf
输入后该值就被存储在rbp-0xc
所指的栈空间中,然后通过mov
将输入的值取出放入eax
中,并与分支的最大值5
作比较,并可能产生两种结果:
若输入的值
> 5
:执行ja
指令继续进行跳转
根据其反汇编代码可以看出,由于我们没有设置default
语句,所以直接跳转到结尾,将返回值0
放入eax
中,释放栈空间进行返回
- 若输入的值
< 5
:接着向下执行
跳表
接着向下执行便涉及到跳表的介入了,一个跳表的表项占4
字节,且为连续存储,所以可直接通过case
后跟的值就相当于索引号,跳表首地址 + 索引号 * 4就是对应表项的位置,所以 索引号 * 4其实就是对应调表表项的偏移地址
lea rdx, [rax*4]
该指令计算的便是偏移地址,由于我们的输入的case
值为2
,所以其跳表表项相对于跳表首地址的偏移应该为2 * 4 = 8
该值被放入RDX
那么接下来便是取出跳表的首地址,并将其放入RAX
仅存其中
lea rax, [rip + 0xe61]
(0x51e3 + 0x61e = 0x6044
)
动态调试看跳表的地址为:0x555555556044
以4
字节为一个单位,查看该内存区域:
与VC
中的跳表不同,此处并不是每一个case
代码块的地址,而更像是偏移
mov eax, dword ptr [rdx + rax]
解析来便是取出当前case
索引对应的跳表表项*(0x555555556044 + 2 * 4)
也就是跳表中的第二项0xfffff1c7
接下来的cdqe
的作用是:将EAX
中的32
位数值的符号位扩展到64
位RAX
寄存器中高32
位的每一位,所以扩展后为
这时候精髓的部分就要到了
来细看这三步
lea rdx, [rip + 0xe55]
此处又将跳表的首地址放入了 RDX 中,为啥是跳表的首地址?可以来计算一下:
刚刚在
0x5555555551dc
处执行了lea rax, [rip + 0xe61]
获取到了跳表的首地址,也就是说跳表的首地址为0x5555555551e3 + 0xe61
而在此处计算出的地址为:
0x5555555551ef + 0xe55
=0x5555555551e3 + 0xc + 0xe55
=0x5555555551e3 + 0xe61
由此推出执行完该语句rdx
中将存放跳表的首地址,也就是0x555555556044
add rax, rdx
这一句的含义我想不用过多说明,但这一句就表示其实跳表中的每个表项存储的就是偏移量,而且是每个
case
代码块相对于跳表的偏移量,而且这个偏移量是有符号的由于
RAX
中存储的有符号数,所以:RAX + RDX = 0xfffffffffffff1c7 + 0x555555556044 = 0x55555555520B
回看其反汇编代码,这正是第二个
case
的代码块接下来便
jmp
到目标位置即可
但我们再跟过去看一眼
lea rax, [rip + 0xe01]
的作用在于将我们要输出字符串的首地址放入EAX
中,准备被printf
输出,这没什么稀奇的
但注意这个字符串所在的位置,也是rip+0xe
什么什么,那么如果在当前位置想要索引到跳表的首地址应该是怎么样的呢,应该是:
0x555555556044 - 0x555555555212 = E32
也就是[rip + 0xe32]
这也足以表明跳表所处区域与常量字符串所处的区域基本为同一区域(图中红色方框中的虚拟地址范围中)
此时还有一个问题,我们可以发现跳表中的第0
项是不会被使用到的,那为什么不将第一项放到0
号索引处依次类推呢?
这是为了方便直接根据case
后所跟序号计算偏移地址,所以才会空出第0
项,转而从第1
项开始执行
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
