本篇文章介绍对于windows第二版的栈展开,我个人对于其中用到的数据结构和它的工作流程的推测
windows第一版的栈展开,在windows的官网有介绍:
07fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9k6h3q4J5L8W2)9J5k6h3#2A6j5%4u0G2M7$3!0X3N6q4)9J5k6h3y4G2L8g2)9J5c8Y4A6Z5i4K6u0V1j5$3&6Q4x3V1k6U0M7s2m8Q4x3V1k6T1N6h3W2D9k6q4)9J5c8X3g2^5j5$3g2H3N6r3W2G2L8W2)9J5k6r3S2S2L8X3c8D9K9h3&6Y4i4K6u0V1P5o6j5@1i4K6y4r3N6X3W2W2N6#2)9K6c8r3#2K6N6X3y4Q4x3X3b7I4y4U0l9`.
关于第二版的栈展开,我找到的资料不是很多。对其做的一些研究,在此记录一下。
第二版是指,当UNWIND_INFO的version字段等于2时。
第二版相比于第一版,在展开时,需要更少的检查/判断就可以知道是否在epilog中,并且当展开一个PC处于epilog范围内的函数时,也无需再检测PC后边的汇编指令。而为了达成这些目的,第二版在数据结构上添加了更多的数据,对编译器生成的代码,也做了更严格的要求。
在数据结构上,第二版新加了一个操作码,专门用于表示epilog的大小和位置。也就是说,对于第二版而言,一个函数中,所有的epilog,都已在数据结构中清晰的标明出来了,只要简单的判断一下,当前的PC是否在其范围内就可以了。而不用像第一版那样,需要试探执行来判断。关于新加的操作码,我们接下来再说。
对编译器的要求上,有两个改变,一个是操作码的顺序,一个是对汇编代码的要求。在操作码顺序上,相比于第一版中比较自由的排列,第二版中所有的push要在表的最后(详细参见下面)。在对汇编代码的要求上,在epilog中,归还栈的操作只能出现一条(比如,add rsp,0x10)。 而pop和归还栈操作中间的位置,就是要写在数据结构中的epilog的大小。这样要求的原因,是因为,save操作即使多次进行也不影响最终结果,第二版让所有的save操作在栈归还之前来完成,栈归还只有一条。执行完栈归还呢,就帮忙把剩下的pop模拟执行了;如果没执行栈归还呢,就把所有的操作码都执行了,(这时无非是多几次save操作,这是不影响结果的),再归还栈,pop。
接下来说下具体的技术细节,新加的操作码,网上的给它起的名字是UWOP_EPILOG,值为6。当函数块(RUNTIME_FUNCTION)有epilog时,需要添加UWOP_EPILOG。第一个UWOP_EPILOG表示epilog的大小(函数中所有epilog的大小都需要相同,这是对编译器的要求),之后的UWOP_EPILOG依次表示本函数块中每个epilog的位置,此位置通过与函数块结尾的距离表示。如果设置了标志(具体参见下面),第一个UWOP_EPILOG也会参与表示epilog的位置,此时每个UWOP_EPILOG表示一个epilog。表示位置的UWOP_EPILOG是按epilog位置有序存放的。当UWOP_EPILOG的个数为奇数时,需要添加一个空的UWOP_EPILOG来进行对齐。空的UWOP_EPILOG里,除了UnwindOp字段为UWOP_EPILOG,其余字段均为0。
第二版对于操作码的顺序,如下表所示:
UWOP_EPILOG(多个或0个,在最前面)
UWOP_其他类型(多个或0个,可以为UWOP_ALLOC_SMALL)
UWOP_PUSH_NONVOL(多个或0个)
UWOP_ALLOC_SMALL(1个或0个,大小固定为8字节,用于模拟popfq)
UWOP_PUSH_MACHFRAME(1个或0个,在最后面)
第一个UWOP_EPILOG的OpInfo字段,有两个取值,1或0。如果为1,则表示第一个UWOP_EPILOG也参与epilog位置的指定,第一个epilog的位置由CodeOffset字段来指定。
第二个以及之后的UWOP_EPILOG,他们的OpInfo字段做为高4位,和CodeOffset字段共同组成一个12位的偏移。
指定epilog位置的偏移,是相对于RUNTIME_FUNCTION.EndAddress(也就是函数块结束位置)的向上偏移,指明了epilog的起始位置
接下来说一下展开过程。和第一版一样,也是先判断是否在epilog里。如果在epilog里:
遍历链,找到表中第一个push操作,然后开始往后遍历,边遍历边数PC走过了多少个字节。此字节数,是从epilog的起始位置算起的,所以可以用来查找真实PC执行到了哪里。当模拟执行的PC到达真实PC之后,开始执行模拟的pop操作。执行完所有表中的pop后,同理执行UWOP_ALLOC_SMALL和UWOP_PUSH_MACHFRAME。都执行完后,模拟ret指令。
如果不在epilog里:
遍历链,跳过每个节点中,遇到的UWOP_EPILOG,执行其他的操作码。像第一版一样,如果在prolog中,也要跳过还未执行的指令,如何跳过的算法和第一版相同。
另外,链中的每个函数块,他们的版本要一样,或者都为1,或者都为2。
参考资料:
reactos中RtlVirtualUnwind函数的实现,里面有对于第一版处理流程的代码:819K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1L8%4S2&6k6$3g2F1i4K6u0W2M7X3g2S2j5%4c8G2M7#2)9J5k6h3!0J5k6#2)9J5c8X3b7^5i4K6u0r3k6o6u0X3i4K6u0r3N6h3&6%4K9h3&6V1i4K6g2X3z5r3y4Q4x3X3g2Z5N6r3#2D9i4K6t1K6j5e0l9K6j5K6V1I4j5U0k6U0y4o6x3%4x3o6j5$3x3U0M7J5k6h3u0U0x3X3x3J5k6X3k6X3x3o6f1I4j5e0c8U0
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课