最近有那么一个想法,要开发一个东西叫LINE。我们都知道linux上有一款很牛逼的软件叫WINE(WINE Is Not Emulater),可以在linux上运行windows程序。LINE(LINE Is Not Emulater)就是WINE的反面,让linux程序在windows上运行。http://gussing.cnblogs.com/

WINE之所以可行是基于这么一个事实:windows程序都是建立在windows API基础之上的,ntdll.dll, kernel32.dll, user32.dll等api把windows系统调用全部封装起来,基于API的应用程序可以完全不知道内核的细节。WINE就干了两件事情:1.开发一个PE loader可以load windows程序,2.开发一整套windows api供windows程序使用。据我所知,WINE牛逼到了绝大多数windows API都已经实现的程度。WINE的大体架构如下:http://gussing.cnblogs.com/

wine

后来又出现一个叫“兼容内核”的项目,他们打算把WINE代码里有系统调用的部分全部移入内核。我们知道linux系统调用和windows系统调用是不可能一一对应的,比如说NtCreateProcessEx系统调用想在linux上实现,可能会有不止一个linux系统调用。倘若把windows系统调用直接在linux内核里实现,那么次数就会减少到1,效率就会提高。另外,用户态很难办到的一些事情在内核态内核导出函数的帮助下,也会变得相对容易些。有兼容内核之后的大体架构如下:http://gussing.cnblogs.com/

unified

如上图所示,兼容内核修改了WINE DLL, 把“用linux系统调用模拟windows DLL”的机制换成了"在linux上打通int 2e,模拟出原生windows DLL”。http://gussing.cnblogs.com/

而LINE之所以(被我认为)可行,是基于这么一个事实:大多数linux程序都是基于glibc之上的,与windows api类似,glibc包装了linux的系统调用,如果可以把linux系统调用界面实现一遍,那么glibc应该就能运行在windows上,相应的大多数*原生*linux程序也可以在windows上运行。http://gussing.cnblogs.com/

 这里不得不提一款几乎类似的程序:cygwin。cygwin在windows上模拟了glibc的功能,叫做cygwin1.dll,通过port到windows上的gcc.exe把带源代码的linux程序都重新编译一遍,也做到了在windows上运行linux程序,如下图所示:http://gussing.cnblogs.com/

cygwin

大体架构和WINE有异曲同工之妙,但是也有很明显不一样的地方,那就是cygwin运行的是PE格式的linux程序,省掉了做elf loader的麻烦。为什么WINE不编译一套elf格式的windows程序呢?很简单,因为没有源代码。http://gussing.cnblogs.com/

最后讲一讲我预想中LINE的架构:http://gussing.cnblogs.com/

LINE

如上图所示,先得有一个elf loader,把原生linux程序load进程空间里,原生linux程序要和glibc打交道,所以也要把glibc load进来。glibc有两部分,一部分在用户态就能做完,一部分需要通过系统调用进入内核,所以LINE里有一个内核态驱动,实现了int 80系统调用的支持。linux里有300多个系统调用,我一个人全部做完是不可能的,且不说时间不够,有些调用根本困难的要命,所以系统调用例程被分为两部分:一部分在内核里实现,一部分未实现。未实现的部分就用APC弹回到用户态,从cygwin1再走一遍。

如此一来,LINE就和WINE一样,可以执行*原生*linux程序了。其实最开始我还想着另一种路径,那就是直接修改cygwin让它走int 80 路径,这么一来glibc相关的一系列讨厌的问题(相信我,还真是很多很讨厌)都可以避免的,还可以最大限度利用已有资源。不过这条路径少elf loader和runtime lib,能学到的东西就会少很多,所以被我否掉了。http://gussing.cnblogs.com/

LINE这个项目我一直是利用业余时间在做,不知道能到什么程度。有时候想想,连我自己都很期待呢哈哈。

posted @ 2009-10-30 16:48 gussing 阅读(116) | 评论 (1)编辑

有了前面的介绍,堆管理的大体框架已经清楚了,但为了使内容完整,我们还是需要看看释放内存时具体都干了些什么。gussing.cnblogs.com

函数RtlFreeHeap接受一个PVOID的参数作为内存地址,却不需要指定内存的大小。这是一个很有趣的地方,malloc,HeapAlloc等申请内存的函数都会指定所需内存的大小,但相应的free函数和HeapFree函数都不需要指定大小。我在面试应聘者的时候很喜欢问这个问题:请问为什么不需要指定就可以释放正确大小的内存?很遗憾照着我的统计数据,9成以上的人回答不出来,这9成里面又有9成压根就没去想过这些问题,也不知道是不乐意,不屑与,还是不舍得去关心。个人认为作为IT从业人员,不管是铁了心要一直当码工也好还是有更高的最求也罢,即使是出于职业道德的考虑偶尔也是要去了解下代码背后的秘密的。偏题了,让我们继续说正题吧。gussing.cnblogs.com

如上面所说,RtlFreeHeap函数接受PVOID类型的函数地址却不需要内存大小,那它如何确定本次需要释放多大内存呢?答案就是:每次申请到的内存都有带一个管理头,里面有指定本内存块的大小gussing.cnblogs.com

image

返回给用户的内存块,是管理头之后的部分,也就是说,每次内存分配的时候实际分配的大小比所要求的要大。考虑到c runtime也有自己的堆管理器,这部分额外内存可能会不小。free函数和RtlFreeHeap函数就是从这些额外的管理区域里读出内存块大小的。gussing.cnblogs.com

RtlFreeHeap函数接收到用户内存的地址后,做的第一件事情就是将地址往前移8字节,指到内存块的管理头BusyBlock。然后检查内存的完整性,比如有没有溢出之类,具体是检查BusyBlock地址的末3未,因为堆申请是以8字节为单位的,所以末3位一定都是0,否则就是被破坏了,需要理解抛异常通知进程。实际的溢出检测比这个复杂多,暂且不谈。经过一些个检测后,堆管理器认为本次操作安全,可以继续下去,于是又根据BusyBlock->Size进行的分情况处理又开始了,需要注意的是BusyBlock->Size是以8为单位的内存区域的数量,就是上一次所说的index:

  • BusyBlock->flag & HEAP_ENRY_VIRTUAL_ALLOC不为0:gussing.cnblogs.com

表明此堆块是直接从虚存里申请出来的,需要直接调用ZwFreeVirtualMemory函数释放,并从VirtualAllocBlock队列里移除相应的项。否则:

  • BusyBlock->Size小于128gussing.cnblogs.com

先检测LookAsideList是否为空,不为空则插入到LookAsideList[BusyBLock->Size]中去。若为空,则搜索FreeList[BusyBlock->Size]队列,看此队列中时候有其他block,若有则取出,与BusyBlock合并,合并完后重新根据BusyBlock->Size大小分类进行。之所以要与旧block合并,是为了最大限度的减少内存碎片。

  • 128 <=BusyBlock->Size< 512gussing.cnblogs.com

普通的FreeList已经没法容纳这么大的堆块了,所以需要插入到FreeList[0]中去。

  • BusyBlock->Size >=512 且堆中记录的总空闲大小没有达到64KBgussing.cnblogs.com

将BusyBlock分块,分别插入到相应的FreeList里去

  • BusyBlock->Size >=512 且堆中记录的总空闲大小超过了64KBgussing.cnblogs.com

堆里已经积累了太多内存,需要调用RtlpDeCommitFreeBlock,释放虚存。RtlpDeCommitFreeBlock函数会把BusyBlock里占满一整个page的内存释放掉,余下的还是放回到堆管理器里去。如下图:

image

区域2,3,4能占满一整个page所以释放,1和5放回到堆管理器里。这么做的原因是虚存的申请和释放都是以page为单位的,这是最小粒度。

到这里为止,堆的申请和释放都完了。要完整了解堆管理器至少还需要看RtlCreateHeap和RtlDeleteHeap两个函数,有兴趣的同学可以自行围观ReactOS相关代码。

posted @ 2009-10-01 21:25 gussing 阅读(153) | 评论 (0)编辑

ring3里的API HeapAlloc, HeapFree在Kernel32.dll里实现,最后都转到NTDLL.dll里的RtlAllocHeap函数和RtlFreeHeap函数。ring0里要操作堆直接调用ntdll.exe里的RtlAllocHeap函数和RtlFreeHeap函数。这两处地方的两个函数名字一样,所作事情也基本没差别,但千万记住它们是不同的,一个在ring0,一个在ring3。gussing.cnblogs.com

RtlAllocHeap接受一个UINT类型的参数,指明本次操作需要申请多大的内存。nt的堆管理以8字节大小为单位,不管你想申请多大的内存,它返回的都是8倍数大小的区块。再加上每个堆块都需要的管理域(块头),堆管理器内部算出来需要申请的内存大小AllocatSize=(size/8+1)*8(16,24,32。。。),相对应的index=AllocateSize/16(1,2,3…)。根据这个index我们可以在LookAside list组或FreeList组里定位到需要的空闲队列。考虑到队列组只有128大小,index必须小于等于127才能在这两个组内搜索;若index大于127且小于64k,则在FreeList[0]里搜索;若index大于64k的阈值,则直接调用ZwAllocateVirtualMemory申请虚存。让我们分情况看:gussing.cnblogs.com

  • index<128gussing.cnblogs.com

先搜索LookAsideList[index],若有合适的,直接返回。LookAsideList[index]里什么也没有,则搜索FreeList[index],若还是没有,则顺着FreeList[index+1], FreeList[index+1]一路找下去,直到找到足够大的块为止。假设找到的块是Block1, 因为它比我们需要的AllocateSize要大,所以就需要进行拆分,前半部为AllocateSize大小返回给调用者,后半部大小BlockSize看情况处理:倘若Block1是所在队列里的最后一块,就调用RtlInsertFreeBlock插回到FreeList里去。RtlInsertFreeBlock会根据传入的block大小进行拆分操作,若block比127大,会分成几份小于127的插到相应的队列中。若Block1后边还跟着一块空闲块,就把该块从相应的list里取出,跟Block1合成一块,然后调用RtlInsertFreeBlock。但是如果搜完整个FreeList都找不到,就需要做下面的事情:

  • 128<=index<64kgussing.cnblogs.com

index已经超过了LookAsideList和FreeList的范围,或者这俩队列里都没有合适的块,所以没法从它们里取,但是它又没大到一定程度,直接申请虚存也不合适,这时候就需要FreeList[0]出场了。如前一篇所说,算出来的block index是不可能为0的,所以FreeList[0]就划出来专门存放大块内存。FreeList[0]队列里的堆块和其他队列不同,因为他们的大小是不一致的。FreeList[1]队列里的堆块block index一定是1,FreeList[2]队列里的堆块Block index一定是2,但是FreeList[0]里堆块block index可以是128到64k间的任何数值。从FreeList[0]申请内存时,需要顺着该队列一路搜索直到找到足够大的Block。若能找到这样的块Block2,则将之取出,做与前面Block1一摸一样的拆分。若找不到足够大的堆块,就要在本Segment的保留内存区域里提交一块相应大小的块,然后返回。若倒霉到家连Segment里保留的内存区域也用完了,则申请新的Segment。新申请的Segment内存大部分都是保留的,只提交了AllocateSize大小的块,以便返回给用户。gussing.cnblogs.com

  • index>=64kgussing.cnblogs.com

申请如此巨大的内存,无论哪个队列都派不上用场了,所以直接申请虚存,申请到的块保留在VirtualAllocatedBlocks队列里,释放的时候也是直接释放虚存,而不是插入到相应的队列里缓存起来。

最后让我们看看批发的大块内存时如何被打成一堆堆小碎片的。起初的时候Heap里只有一个Segment,保留了一片巨大的内存区域但是没提交,这时候我们开始申请小内存:找LookAsideList,没有;搜索FreeList,也没有;开始搜索FreeList[0],还是什么也没有;只好到Segment里提交一块,这次总算有了。就这样经过多次小额申请后,Segment里的大块内存就被分成了一片片的小碎片。gussing.cnblogs.com

posted @ 2009-09-28 17:42 gussing 阅读(99) | 评论 (0)编辑

严格的说,用户态和内核态都有堆管理相关的内容,两者用的是同一份代码,稍微有些不同的地方就用宏隔开。在windows上写c程序会有不止一个的“堆管理器”介入,比如malloc, free用c runtime的堆管理器;用户态的HeapAlloc,HeadFree等函数用ntdll.dll里的堆管理器;内核态的RtlHeapAlloc,RtlHeapFree等用ntoskrnl.exe里的堆管理器。感觉蛮混乱的,相比之下linux内核里没有所谓的“堆管理器”,堆相关操作全在glibc(linux上的c runtime)里实现,内核中要动态申请(小块)内存,用的是kmalloc函数,而且申请到的是物理内存。gussing.cnblogs.com

堆管理的实质是:系统一次性批发很大一块内存留着,然后慢慢零售给应用程序。应用程序释放的内存也不直接还给系统的虚存管理系统,而是先给堆管理器,等攒够一块大的再一次性归还。有4个比较重要的队列负责实现此种批发零售:Segment队列,LookAside队列组,FreeList队列组,以及VirtualAllocatedBlock队列。gussing.cnblogs.com

Segment队列是一个静态数组,大小为64,充当批发商的角色,其他队列保留的内存其实都在Segment数组里,它们只是再引用而已。gussing.cnblogs.com

FreeList队列组是一个内存块的缓冲区域,拥有128个队列。应用程序释放的内存会先保留在FreeList里,之后程序再申请内存会先找FreeList里保留的。比如某程序释放了6单位大小的内存,它将被插入到FreeList[6]队列里。下次程序又申请6单位大小的内存,就可以直接取FreeList[6]队列里的内存块使用的。当然,实际情况比这个要稍微复杂些,因为你申请6单位大小内存的时候FreeList[6]可能是空的,这时候你就需要顺着FreeList[7],FreeList[8]这样一路找下去,直到找到合适的块(假设为9)为止。然后堆管理器会把这个块切割成两份,一份6单位大小返回给程序;另一份3单位大小的额外内存。原本这3单位全是可以直接使用的,现在第一单位被征用作为块头用于管理,然后插入到FreeList[3]中去。FreeList队列组中,第0号队列是比较特殊的,因为堆管理器里保存的内存块都带1单位的管理域,它的大小最小也就是1,不可能是0,于是0号队列就被空出来了。再考虑到FreeList中只有128个数组,大于128的内存块就没地方去了,正好FreeList[0]空着,就放到那里去吧。所以FreeList[0]中保留了大于128,小于某阈值的内存块。gussing.cnblogs.com

LookAside队列组长的和FreeList队列组很像,也是128个队列,但区别在于LookAside队列不会迁就应用程序,程序在这个队列里申请内存,有合适的就给你,没合适的就拉倒,你想让他把一块大的分割出来给你?对不起,办不到。这么做可以缓解内存碎片的问题:倘若一个程序申请释放小块内存很多很多次,那么FreeList里存着的就基本上全是小块内存了,这时候你要申请一块大的,即使内存其实还足够多,堆管理器也没法拿到合适的。而LookAside队列这样不给你分割的,即使你申请释放小内存很多次,对大内存也一点影响没有。gussing.cnblogs.com

应用程序每次操作的内存大小一般都是很小的,几十字节,几百k都是最常见的,但你也阻止不了人家申请巨大内存啊,比如有个人就是要申请20M内存,你还能不给他?这么大的内存块,FreeList和LookAside是没地方给他保存了(FreeList[0]中保留的也必须小于某阈值啊,不可能什么样的大内存都给你放),堆管理器就索性绕过所有的队列容器啥的,直接给它申请虚存,,申请到的内存就放在VirtualAllocatedBlock队列中。gussing.cnblogs.com

堆管理器的关键数据结构就是这些,围绕这些结构进行的具体操作,我们以后再谈。gussing.cnblogs.com

posted @ 2009-09-26 16:49 gussing 阅读(153) | 评论 (0)编辑

一个进程在其生命周期内可能会打开很多个内核对象,这些对象需要得到很好的管理才能保证效率。NT内核使用Table来保存这些打开对象,该Table的指针存放在EPROCESS->ObjectTable里。gussing.cnblogs.com

当我们访问ObjectTable时,需要确定的有两个信息:Table地址和表的级数。Object Table并不总是一个巨大而平坦的线性列表,因为有时候内核对象的数量会非常之大,一个线性列表不够用。实际情况是,当对象数目大于512时EPROCESS->ObjectTable指向的是一个二级表;而当对象数目超过512*1024时,EPROCESS->ObjectTable指向的是一个三级表。Object Table的值一定是8的倍数,也就是说末三位一定是0,所以EPROCESS->ObjectTable字段的末三位可以用来记录额外信息也就是表的级数,0表示一级表,1代表二级表,2代表三级表。gussing.cnblogs.com

一级表的结构:

image

整个表的大小算下来正好4096B,为1 page。内存管理组件是以页为单位进行管理的,把一段信息的大小局限在一页内可以有效提高效率。

插播一条广告:为了学习nt内核,我写了一个山寨 ProcessExplorer,可以查看进程信息,线程信息,handle列表,已加载dll列表,已加载驱动列表等等信息,所有信息都是手工收集,用 过的API只有DeviceIoControl一个,当然,内核导出函数还是用了不少的。能把指定dll注入指定进程,还能强杀某些杀毒软件。注意!目前 只支持xp sp3,其他版本的windows未经测试。

该玩具源代码放在http://code.google.com/p/yasi/上,欢迎各位去取。另外,这是本人习作,高手不准笑。。。

 

二级表的结构:gussing.cnblogs.com

image

在二级表中,ObjectCode指向的不是对象表本身,而是1024个对象表的地址组成的指针数组。每个表项都是HandleTable*类型的指针,指向一个一级表。

这里还有一个陷阱:并不是指针数组里的每一项都是有效值。比如你之前打开了三个内核对象,然后关闭第二个,那么数组的第一项和第三项仍然有效,第二项却成了无效指针,这就是所谓的“空洞”,遍历ObjectTable时一定得注意跳过这些空洞,否则等待你的就是蓝屏死机。

三级表结构gussing.cnblogs.com

image

 

 

 

 

二级表和三级表中的高层表是一个指针数组,每项4字节,所以总共可以容纳4096/4=1024项。

Object Table的结构介绍完毕,其实相当简单。gussing.cnblogs.com

再来看HANDLE_TABLE_ENTRY里到底存了什么。HANDLE_TABLE_ENTRY结构定义如下:

typedef struct _HANDLE_TABLE_ENTRY
{
        union{
                PVOID Object;
                ULONG ObAttributes;
                PVOID InfoTable;
                ULONG Value;
        }u1; gussing.cnblogs.com
        union{
                struct _s1
                {
                        union{
                                ULONG GrantedAccess;
                                struct _s2{
                                        unsigned short GrantedAccessIndex;
                                        unsigned short GreatorBackTraceIndex;
                                }s2;
                        }u2;
                }s1;
                int NextFreeTableEntry;
        }u3;
}HANDLE_TABLE_ENTRY,*PHANDLE_TABLE_ENTRY;

这是一个8字节大小的内存区域,前四个字节是Object Header的地址,后四个字节为一些管理域。需要注意的是,HANDLE_TABLE_ENTRY->Object也是8字节对齐的,所以末三位清零才是真正的Object Header的地址。

最后让我们看看从HANDLE到Object的全过程:gussing.cnblogs.com

image

posted @ 2009-08-30 16:30 gussing 阅读(139) | 评论 (0)编辑
     摘要: 有那么一段时间,“对象”基本上是当时IT届最流行的词语,无论什么东西都要搭上“对象”的概念才够体面。NT核就诞生在那个年代,所以在其设计概念中有“内核对象”这么一个牛逼的物件。几乎所有的windows内核组件,包括进程,线程,文件,设备等都属于内核对象,它们有一组共有的数据以及几个函数指针以提供抽象的访问,基本上c语言要玩&l...  阅读全文
posted @ 2009-08-22 19:53 gussing 阅读(149) | 评论 (0)编辑
     摘要: ntoskrnl.exe导出了很多内核例程供驱动开发人员方便的使用,它也隐藏了很多很强大的历程,防止驱动开发人员过于方便的用它们。比如PspExitThread,这个函数没什么特别的,但配合APC使用它就能杀死绝大多数的进程,包括一些杀毒软件在内。gussing.cnblogs.com 那么如何才能找到这些未导出的函数并使用它们呢?答案就是反汇编,借助强大的windbg,加上一点点汇编基础,操作系...  阅读全文
posted @ 2009-08-18 14:35 gussing 阅读(181) | 评论 (1)编辑
     摘要: PsTerminateProcess函数用于结束一个进程,其声明如下 NTSTATUS NTAPI PsTerminateProcess(IN PEPROCESS Process, IN NTSTATUS ExitStatus) 其中第一个参数是PEPROCESS的指针,如果你只知道pid,可以通过PsLookupProcessByProcessId 获得,而第二个参数指定退出状态码。 Ps...  阅读全文
posted @ 2009-08-11 17:44 gussing 阅读(121) | 评论 (0)编辑
     摘要: ReadProcessMemory函数用于读取其他进程的数据。我们知道自远古时代结束后,user模式下的进程都有自己的地址空间,进程与进程间互不干扰,这叫私有财产神圣不可侵犯。但windows里还真就提供了那么一个机制,让你可以合法的获取别人的私有财产,这就是ReadProcessMemory和WriteProcessMemory。为什么一个进程居然可以访问另一个进程的地址空间呢?因为独立的只是低...  阅读全文
posted @ 2009-07-01 19:23 gussing 阅读(649) | 评论 (0)编辑
     摘要: 在msdn中关于GetOverlappedResult的描述如下:GetOverlappedResult FunctionRetrieves the results of an overlapped operation on the specified file, named pipe, or communications device.BOOL WINAPI GetOverlappedResul...  阅读全文
posted @ 2009-06-30 20:24 gussing 阅读(640) | 评论 (0)编辑