浅谈: switch语句汇编跳转表的解码方式

示例

设一个C函数:

void switcher(long x) {
	switch(x) {
		...
	}
	...
}

其由gcc编译后开头的一段asm如下:

;void switcher(long x)
;x in %rdi
switcher:
	addq	$1, %rdi
	cmpq	$8, %rdi
	ja	    .L2
	jmp		*.L4(,%rdi,8)
	...

生成的跳转表如下:

L4
.quad    .L9
.quad    .L5
.quad    .L6
.quad    .L7
.quad    .L2
.quad    .L7
.quad    .L8
.quad    .L2
.quad    .L5

根据这些信息,如何确定\(\texttt{switch}\)体的内部结构?

  1. 确定x的最大取值:假设跳转表寻址在C伪代码表示中用来保存分支段地址的数组为jt,其下标索引由变量\(\texttt{index}\)负责。根据asm的第二行推知,\(\texttt{index = x + 1}\);根据asm第二、三行推知,若\(\texttt{index}>\texttt{8}\)则控制流直接跳到\(\texttt{default}\)分支,进而得出只有当\(\texttt{index} \leq 8\)时才会进入case判断,也即\(\texttt{x} \leq 7\)

  2. 确定x的最小取值:由于数组索引由0开始,那么当x为可能的最小值时,会有\(\texttt{index} = \texttt{x}+\texttt{1}=\texttt{0}\),得出\(\texttt{x}\)最小取值为-1.

  3. 在跳转表每个标号地址后标注对应的数组索引。已知索引是由\(\texttt{index}\)确定的,进而在后面标注x的对应case取值。由于.L2对应的是\(\texttt{default}\)分支,则说明对应的case不存在,此时标一个*号说明该case实际上不存在。

    .L4
      .quad    .L9  <-  jt[0] index=0 -> x=-1 -> case -1
      .quad    .L5  <-  jt[1] index=1 -> x=0  -> case  0
      .quad    .L6  <-  jt[2] index=2 -> x=1  -> case  1 
      .quad    .L7  <-  jt[3] index=3 -> x=2  -> case  2
      .quad    .L2  <-  jt[4] index=4 -> x=3  -> case  3*
      .quad    .L7  <-  jt[5] index=5 -> x=4  -> case  4
      .quad    .L8  <-  jt[6] index=6 -> x=5  -> case  5
      .quad    .L2  <-  jt[7] index=7 -> x=6  -> case  6*
      .quad    .L5  <-  jt[8] index=8 -> x=7  -> case  7
    
  4. 至此已经可以写出switch内部大致结构了,可以先用伪代码表示一下。
    注意标准了*号的分支不存在,所以不写出来。另外某些case对应同一个标号地址,则将它们放到对应的标号段中:

    L9:
    	case -1;break;
    L5:
    	case 0
    	case 7; break;
    L6:
    	case 1; break;
    L7:
    	case 2
    	case 4; break;
    L8:
    	case 5; break;
    

总结一下,以上步骤做完后,便可以定出不存在的case、存在case坐落的标号段。不过标号段的顺序现在是无法确定的。在顺序未确定之前,对应的switch源码可写为:

void switcher(long x) {
	switch(x) {
	case -1:break;

	case 0:
	case 7: break;
	
	case 1: break;
	
	case 2:
	case 4: break;
	
	case 5: break;
	}
	...
}

亦可写为:

void switcher(long x) {
	switch(x) {
	case 0:
	case 7: break;
	...
	case -1:break; // 将case -1放在最后。
	}
	...
}

我们在上面列出了asm代码的前几行,将后面的一些信息补全的话,switch内部的顺序便可固定下来:

;void switcher(long x)
;x in %rdi
switcher:
	addq	$1, %rdi
	cmpq	$8, %rdi
	ja	    .L2
	jmp		*.L4(,%rdi,8)
	.section		.rodata
  .L6:
  	...
  .L5:
  	...
  .L7:
  	...
  .L9:
  	...
  .L8:
  	...

现在根据标号段的顺序,便可写出对应case的顺序了:

void switcher(long x) {
	switch(x) {
// .L6
	case 1: break;
// .L5
	case 0:
	case 7: break;
// .L7
	case 2:
	case 4: break;
// .L9
	case -1:break;
// .L8
	case 5: break; 
	}
	...
}

值得一提的是,像case2、case4这种同标号段呢的分支顺序可以互换,因为它们对应的是同一种情况。

posted @ 2022-07-30 00:18  elexenon  阅读(384)  评论(0)    收藏  举报