现在的位置: 首页 > 综合 > 正文

编译器stack操作

2013年11月30日 ⁄ 综合 ⁄ 共 4724字 ⁄ 字号 评论关闭
编译器stack操作

     标题起了这么一个奇怪的名字,呵呵,其实这个跟游戏制作没有什么大的关系,跟底层驱动程序到是有很大的关系.
     众所周知有个 crt ,他提供了很多有用的函数,比如 printf ,比如 strlen 等等,同时大家也知道 c++ 也依赖这个 crt ,而同时 c++ 里面的全局静态变量要在 main 函数运行之前完成初始话,相信也有人知道,其实我们自己写的 main 函数并不是在进程加载执行的时候操作系统调用的第一个用户态函数.等等的这些都是编译器链接器作的手脚.好在 ms 提供了全部的完整的 crt 的实现,花点时间就能了解到编译器究竟作了些什么额外的操作.
     这里一个额外的话题就是能不能让我们的程序不依赖编译器提供的 crt 呢?答案是肯定的,比如 kernel mode 的 driver 就不会也不能依赖编译提供的 crt ,而 driver studio 却是能用 c++ 语言的,那他是怎么作的呢?自然是自己提供了 crt 的支持,这个说起来就长了,要作的工作很多很多,有的简单(比如 new delete 的实现),有的复杂( c++ exception 的实现),不是这里讨论重点,如果有兴趣可以到 google 上面搜索,能找到不少的文章的.这里主要说的是 nt kernel 对 thread 的 stack 管理.
     一个内存区域在 win32 里面有两个操作,一个叫 reserve 一个叫 commit , 对一段内存进行 reserve 并不引发操作系统的内存分配动作,直到对 reserve 的内存区域进行 commit 操作的时候,操作系统才真正的进行内存描述符的分配,进行调页,详细的信息可以参考 inside windows 2000 和 advanced windows 这些书籍.在编译器里面,对于 stack 同样有这样的操作,/STACK 选项能设置线程的对 stack reserve 和  commit 的默认值,但是 上面说了必须要对 reserve 的内存进行一次 commit(调用一个 win32 api ) 以后,内存才被能被使用,但是我们并没有在自己的程序里面显式的进行 stack 的 commit 调用阿? 自然这也属于编译器的内部操作了.也许有人碰到过这种错误,编译器告诉说有个不能解析的外部符号叫 _chkstk (各个版本的 vc 有不同的名字,也许叫 _chkesp ).这个函数就是关键所在了.也是今天这个文章要讲解的主题.
    说了这么多的废话,不知道大家晕了没有.先作个实验,让你瞧瞧 _chkstk 所作的工作.首先,新建一个工程,选择 win32 console 的吧,最简单的只有一个 _tmain 函数的那种( vs.net 2003 ),然后选择项目->属性->链接器->输入->忽略所以默认库,选择是,然后链接器->高级->入口点,输入 main ,然后选择 c/c++ ->代码生成->基本运行时检查,选择默认,然后打开那个cpp文件,删除_tmain函数,如下输入
      int main()
     {
            volatile int a[10000];
           a[9999] = 0;

            return 0;
     }
     f7 ,出来错误了,error LNK2019: 无法解析的外部符号 __chkstk ,该符号在函数 "int __cdecl test(void)" (?test@@YAHXZ) 中被引用,说明在这个test的函数里面调用了这个 _chkstk 函数,接着再在文件里面加入
      extern "C" void _cdecl _chkstk()
     {
     }

     这次通过编译了,运行瞧瞧,出错了.为什么会这样呢?仔细检查下,没有语法错误阿,数组也没有越界啊,没有道理啊,我不说你也该知道了,问题出在那个 _chkstk 函数里面,既然编译器要求要使用到这个函数,那这个函数就绝对不是像我们自己提供的那个什么都不作的函数那样简单,好了,现在你也该明白 _chkstk函数的重要了,那究竟这个函数是什么样子的呢?是用来干什么的呢?先看他的源代码.新建一个工程,还是 win32 console ,这次什么都不要改,只是在 _tmain 函数里面加入上面 main 函数里面的内容,在 _tmain 函数的开头设置一个断点,转到反汇编,你就能看到类似下面的代码
     00411A13 mov eax,9D08h
     00411A18 call @ILT+250(__chkstk) (4110FFh)

     mov 了一个 eax ,然后调用 _chkstk 函数,跟踪进去,呃,是一个用汇编实现的函数.代码我就不贴上来了,再看看上面对这个描述的说明,你可能已经明白这个函数是干什么用的了,简单的说,他就是在 os 的配合下完成对 reserve 的 stack 在需要的时候进行 commit ,但是他也没有明确的调用进行内存 commit 的函数啊(说了半天都没有提到这个 win32 api 的名字,他叫 VirtualAlloc ),那又是怎么完成的呢? 上面说了,他是要在 os 的配合下面完成 stack 的操作的. 两个关键字 os 和 stack 都是不能少的.那你也许会问了, os 又是怎么完成这个操作的呢?看那个 _chkstk 函数只是循环每一个要访问的 page 作一次读操作( test [ecx],eax )而已啊,难不成这里面有隐藏什么东西?很不幸,被你言中了,关键就在那个 test 操作上面.
     上面说了,内存没有 commit 的时候,并没有建立适当的描述符的,这个时候对这个内存地址进行读写操作都会引发一个 page fault 异常, os 捕获这个异常,检查一定的条件,适合的时候就把这个内存页 commit 了.这就是原理.细心的你也许要问了,那上面两个程序有什么不同呢? os 不是在读取内存时候会进行自动的 commit 吗? 为什么第一个程序就会出错呢? 而第二个程序同样也是读内存啊,怎么就不出错呢?呵,上面也说了, os 会检查一定的条件,这个条件是什么呢? 首先, 能被自动 commit 的内存区域只能是 thread stack,其次, stack 必须连续的增长,不能跨越 page .第一个条件很容易想得通,第二个就不是那么的明显了.先看下来的图
     ____________
     |___________|   <-  当前 esp 所在的 page ,commit
     |___________|   <-  下一个 page ,commit 并且设置了guard 属性
     |___________|   <-  再下一个 page , reserve
     在当前使用的 page 的下一个 page,设置了guard 的属性,访问这个 page 的时候会发生 page fault ,os 检查 page 的 guard 属性,发现是有这个属性的,接着 把第3个 page commit 了,并且设置 guard 属性,这样循环下去就完成了 stack 的自动 commit.如果一个 page 没有 guard 属性,而只是reserve 的话,这个时候 os 就不会进行自动的 commit 了,而是引起一个访问非法的异常.就像我们上面的那个程序一样.编译器在编译的是时候,默认给 thread reserve 1M 的 stack ,并且 commit 2 个 page ,第二个 page 有 guard 属性,而我们的那个程序使用的内存却不是第二个 page ,而是越过了它,访问了后面的 page ,而后面的 page 却是 reserve 的,所以就引发一个非法访问的异常.而后面的那个程序,因为使用了一个循环一一访问每一个 page ,这样在 os 的帮助下就完成了一一 commit page 的任务,明白了么? 事实胜于雄辩,我们来看看是不是这么一回事情.
     这里要还另外的一个程序了,soft-ice,操作我不多说,symbol load加载刚刚编译出来的后一个程序,自动断点到入口,f3切换到源代码和汇编代码混合模式,f8单步进入 _chkstk 函数内部,断点到 test [ecx],eax这一行.page ecx 看看,呃 这个页是 present 的,g ,断点 再 page ecx,不出意外这个 page 应该是 no present 的,这个时候 bpint 0e ,在 x86 平台上面 0e 就是 page fault 中断,然后在 test 语句下面下断点,记录下这个 ecx 的值,接着 g,嗯,断点了,在 0e 中断里面,看看 cr2 的值是不是和刚刚记录的 ecx 的值一样(因为 ss 段的 base 是0)?这个时候不能直接 g 了,bd 那个 int 0e 断点,然后再 g ,回到了test 下面的语句.这些行为都证实了我们上面的说法.唯一没有提到的就是 os 对 0e 中断的处理方式了.可惜小 T 并不是一个逆向工程的高手,不然大可以把 0e 中断处理函数的代码弄出来给大家详细的讲解.不过这些都因为 win 2000 的源代码泄漏而迎刃而解了,在源代码包的 private/ntos/ke/i386 目录下面有个 trap.asm 文件,里面定义 _KiTrap0E 函数就是用来处理 0e 中断的,(有点怕怕,讲这个东西会不会被 fbi 抓,-_-#),这个函数首先保留现场,然后作些检查,push 适当的参数,转调MmAccessFault函数,这个函数在 private/ntos/mm/mmfault.c 里面定义,还算 well documented.哈. 这个函数非常的长,小 T 也没有完全的弄明白全部的 if else 的分支走向,大致的看看,前面的部分是处理 kernel mode 的 page fault,后面从 UserFault 标号开始的才的 user mode 的 page fault,valid 各个 pde pte 以后,来到 1366 行,这里是处理 MM_GUARD_PAGE 的地方,也就是我们这次的关键了.首先清除这个 pte 的 GUARD 标记,然后进入 MiCheckForUserStackOverflow 函数,这个函数在同一个目录下面的 acceschk.c 文件里面,上面有个图跟小 T 画的差不多.这个函数要作很多必须的检查,小 T 也没有详细的分析,只是看了看我们这次的关键部分,重新分配了一个 GUARD 属性的 page (上一个 page 的 GUARD 已经被清除了),同时如果 stack 到了 reverse 的最低一个 page ,这个page 并不被 commit .最后这个函数返回 STATUS_PAGE_FAULT_GUARD_PAGE 值,然后MmAccessFault函数也返回到_KiTrap0E,这个时候 eax = STATUS_PAGE_FAULT_GUARD_PAGE,or 结果非负,跳转到 Kt0e10 ,接着恢复现场,完成 int 0e 的处理.这就是对于 stack 这种能自动 commit 的 page 的操作,对于访问的不是 guard 的 page ,引发一个非法访问异常,也许被高层的 __try 捕获,也许就结束了整个进程.这个部分,小 T 也说得语焉不详,因为小 T 也没有详细的分析过2000的源代码,只是凭借源代码里面的的注释,已经那些符号推测的执行流程.不过大体上应该不会有什么差错,具体的信息能在 inside windows 2000 以及 advanced windows 里面找到.小 T 上面写的只是在自己走马观花的情况下得出来的流程,不一定正确.有兴趣的朋友可以多多研究,虽然 win2000 的源代码泄漏出来的部分,据ms说只有那么不到 1% ,但是,很不幸,没有泄漏出来的东西在小 T 看来都不是那么重要,最最关键的进程线程管理,内存管理,i/o 调度大都展现在各位的眼前,ms 声称有 40个 G 的完整源代码最最精华的关键就在这 40 M上面了.哈,ms 有的哭了.
     也许大家会奇怪,我为什么要写这么一个文章,因为偶现在正研究设备驱动程序,正努力的搞清除编译器内部的动作,哈.

抱歉!评论已关闭.