posted @ 2009-03-12 21:35 gussing 阅读(157) 评论(0) 编辑
摘要: bash和gcc都能运行了,离“可用的系统”又进了一步。今天整理了下代码,放到了google code 上,有兴趣的都可以下载下来看。要是有谁对这也感兴趣,可以在下面留言,一起来玩。如果把讨论范围缩小到x86平台,那么linux和windows的区别,至少在用户态层面的区别,比我们想象的要小很多,所以事实上如果你真想干的话,在windows上实现各类*nix特性并没有想象中那么困难。反过来说,在*nix上实现windows特性也完全能做到。这不是随口一说,前者有cygwin, coLinux,后者有wine,都是成熟的项目。我这个东西跟cygwin的区别前面已经说过很多次了,跟coLinux的阅读全文
posted @ 2011-09-04 02:24 gussing 阅读(969) 评论(7) 编辑

在大约一年前提到过,我想做这样一件事:打通windows上的int 80中断,让原生的linux程序也可以在windows上跑。中间因为公司项目紧,再加上idt的一个小问题困惑了我很久,所以搁置了一段时间。最近觉得周末实在闲的慌又把这项目捡起来了,并且在某次抽烟的时候突然想到“每个处理器都有自己的idt表”这一小常识,idt的问题也就很顺利的解决了。接下来的事情就变得很顺了,按照原定计划先把所有的系统调用转到用户态然后从cygwin1.dll里再过一遍,大约熬了两天夜,一个小小的demo就完成了。

代码参考了很多的开源项目,比如cygwin, glibc, linux kernel, 还有一个死了很久的项目 LINE。这事说起来也好玩,我最初想做这个项目的时候,压根没到sourceforge上去找过,包括实现原理,项目名称等都是自己想出来的。结果做了一半到sourceforge上一搜,居然有个一摸一样想法的东西早在2001年就存在了,连名字都想的一样。然后再到google上一搜,无数人都想到过类似的方案了,而且名字无一例外的打算叫成“LINE”。不得不说在开源的世界里想真正的创点新还真是困难。所幸sourceforge上的这个项目老的不成样了,是2001年的,看代码只能在win98上跑,而且是半成品,连编译都通不过,所以我做的这些事情也不是完全没价值。

不过看我这个项目仍然继承了sourceforge上这个LINE的很多东西,比如把系统调用反射回用户态这事,我原来的打算是插一个APC反射回去,看了下他们的做法,是直接把调用栈上保存的返回地址改成了用户态的一个SyscallHandler,有点类似缓冲区溢出注入的技术,这个方法毫无疑问的更合理更优雅。再比如,绝大多数的系统调用转回到用户态后只是简单的把参数包一包就转cygwin上去了,这部分代码属于纯体力劳动,我就很不客气的也照抄过来了。总之,之前我把这个项目定性为我的“创新”,现在我把它定性为LINE项目的后续开发,虽然少了重新发明轮子的快感,不过坦白我也已经过了追求这种快感的年龄了。

在开发过程中碰到的主要问题有三个:idt的问题,某些较新的linux系统调用没法实现的问题,以及路径问题。我之前设置int 80中断的时候,都是在一个IOCTL里直接就调sidt指令,以为这么做就能改掉系统idt表,实际上由于每个处理器有自己的idt表,这么干只能改IOCTL发起者所在处理器上的idt表,到了运行调试的时候一个int 80中断的发起者到底是不是在该处理器上根本没法保证,所以有很长一段时间里我就处于这么一个状况:设置好idt后一调试,好的;再调试,异常了。搞的我很莫名其妙。后来我在IOCTL处理函数里往每个处理器都插一个DPC,然后在DPC里改idt,这种时好时坏的情况就再也没发生了。

等到用户态的响应函数写的差不多,简单的linux程序比如hello world等都能跑了后,我就开始从原生linux上抓真正的程序过来试。结果一跑问题就来了:linux 2.5.29 及以后的版本新增了几个系统调用set_thread_area, get_thread_area 等等,是为了对NPTL做支持,这几个函数没办法在windows上模拟出来(至少我现在没想到办法),所以我就留空了,直接返回-ENOSYS (事实上190号以后的系统调用我全留空了)。但是现在的glibc还非用它不可,set_thread_area返回-ENOSYS后,glibc就直接打印error退出了。没办法,我只好在虚拟机上装了个redhat 6.0, 然后把那上面的程序和类库考出来用。RH 6.0 那可是史前时代的东西了,不会用到那么高级的东西,应该能跑。

最后还剩一个问题:windows的路径和*nix的路径表示完全不一致,比如linux上的程序想打开/lib/libc.so.6这个文件,在windows上根本就找不到。解决这个问题的方法倒是简单粗暴:我在open等文件操作的系统调用里做了一个小改动,凡是以‘/’打头的路径名,我就把第一个’/’去掉让他变成相对路径,然后再用windows的open函数就能顺利的打开了。直到目前为止这个小技巧还是奏效的,从RH 6.0上拷过来的基本命令都能跑,有图为证:

image

 

当然,这东西现在还只是demo而已,问题还不少。比如跑bash还是有点小问题:bash能启动,但是在bash里打ls命令又是文件未找到。再比如只能在32位机器上跑,再比如很不稳定等。总之,后续还有很多工作要做。

源代码目前还没脸放出来,等整理完后会放到google code上,沿用GPL v2。

posted @ 2011-08-25 15:43 gussing 阅读(1145) 评论(5) 编辑

上回我们留下一个未解的问题,就是当一个IRP的CancelRoutine没有被设置时,CancelIo操作会失败,系统中有可能会留下永远都不会被complete的IRP。在Threaded IRP和non-threaded IRP一节中我们有谈到irp分为线程相关和非线程相关两种。倘若一个永远不complete的irp是非线程相关的,情况会稍微好一点,顶多系统中泄露了一个资源。倘若该irp是线程相关的,那事情就大了。thread IRP由IoManager生成并保留在线程的IRP队列里,负责处理该IRP的驱动在收到下层驱动的Complete事件后不会主动收回IRP的资源而是继续complete给IoManager,由IoManager负责回收,并从线程IRP列表中删除该IRP。一个线程在退出前会遍历等待IRP队列里所有的IRP,直到它们全部被complete为止。倘若其中有一个irp永远不complete,那么线程就永远不退出,无论是ExitThread也好还是_endthreadex也好还是什么邪恶的暴力擦除数据强退也好,全都不顶用。线程不退出,进程也不能销毁(题外话:进程资源的回收动作由最后一个线程退出后发起,所谓的杀进程,其实是用apc给所有线程发起退出操作)。更糟糕的是,操作系统的关机过程都会被堵住,除了关电源,没有其他办法恢复,这一点简直比BSOD还糟糕。我们知道由user mode发起的IO操作最后都会翻译成threaded irp,这就是为什么我在7.1大谈特谈user mode线程的原因:这个陷阱连user mode程序也会掉进去。Bad dog!
要解决这一点方法很简单目标很明确,那就是防止“永远不complete的irp”这种东西出现。一般的做法是加个线程或者timer并设置超时时间,时间一到就cancel这个irp。如果irp由user mode程序发起,那么就调用CancelIo;如果irp由驱动发起,则是调用IoCancelIrp。所有这些动作要生效的大前提是你的irp有CancelRoutine的存在,否则一切都是白搭。所以这里我有个经验要跟大家分享:任何时候都给你的irp设置CancelRoutine,并在CancelRoutine里Complete你的IRP!为方便起见我们选non-threaded irp做个例子,所有的代码都在内核态,免得各位看官看示例代码还要做上下文切换。以下便是代码:

Sending thread:

IoSetCancelRoutine(Irp, MyCancelRoutine);
devext->SentIrp = Irp;

Canceling thread:

if (devext->AllocatedIrp != NULL) {
   IoCancelIrp(devext->SentIrp);
}

cancel routine里的内容都是标准步骤,不赘述。看起来已经完美无缺了,可惜拿到测试组一跑就BSOD,系统抱怨说一个irp被free了两次,肯定是有地方被疏忽了,对,我们很好的处理的例外情况,却漏掉了常规情况:irp也是可以正常complete的!假如我们的CompleteRoutine是这样的:

Completion routine:

PIRP irp;
irp = devext->SentIrp;
devext->SentIrp = NULL;
IoFreeIrp(irp);


它和CancelRoutine里用到了同一个irp,这是典型的多线程重入问题,需要加锁保护。修改后的代码如下:

Sending thread:
KeAcquireSpinLock(&devext->SentIrpLock, ...);
devext->SentIrp = Irp;
KeReleaseSpinLock(&devext->SentIrpLock, ...);


Canceling thread:
KeAcquireSpinLock(&devext->SentIrpLock, ...);
if (devext->AllocatedIrp != NULL) {
   IoCancelIrp(devext->SentIrp);
}
KeReleaseSpinLock(&devext->SentIrpLock, ...);

Completion routine:
PIRP irp;

KeAcquireSpinLock(&devext->SentIrpLock, ...);
irp = devext->SentIrp;
devext->SentIrp = NULL;
KeReleaseSpinLock(&devext->SentIrpLock, ...);

IoFreeIrp(irp);

return STATUS_MORE_PROCESSING_REQUIRED;


又是一个完美的程序,半分钟修掉一个BSOD,还有比这更爽的吗?结果一测试问题更大:系统挂起没有任何反应了。经验告诉我们这是一个死锁:Cancel thread获得spin lock后调用IoCancelIrp,IoCancelIrp最终进入CancelRoutine,而CancelRoutine则调用了IoCompleteIrp并进入Complete routine并试图再次获得spin lock,而该死的spin lock在同一条线程里也是会死锁的,这就是最终原因。
问题出在rip的完成上。设置了Cancel routine和Complete routine后,有两个点可以做irp的完成和回收动作,而这两个点只能有一个被执行。借用网上某牛的代码描述我们可以看到有以下几种情况:

// No cancellation:
//   Cancelable-->Completed
//
// Cancellation, IoCancelIrp returns before completion:
//   Cancelable --> CancelStarted --> CancelCompleted --> Completed
//
// Canceled after completion:
//   Cancelable--> Completed -> CancelStarted
//
// Cancellation, IRP completed during call to IoCancelIrp():
//   Cancelable --> CancelStarted -> Completed --> CancelCompleted


这跟同步还是两回事,同步是指两个点不能同时摸这个irp,一个摸完换另一个则是可以的,而我们要达到的目标是只要irp被其中的任何一个摸过了,另一个就不能再去摸它。为了达到这个目的,我们需要增加额外的变量记录irp被摸了几次这个信息.改造后的cancel过程如下
        if (InterlockedExchange((PVOID)&touched, IRPLOCK_CANCEL_STARTED) == IRPLOCK_CANCELABLE) {

          //
          // You got it to the IRP before it was completed. You can cancel
          // the IRP without fear of losing it, because the completion routine
          // does not let go of the IRP until you allow it.
          //
          IoCancelIrp(irp);

          //
          // Release the completion routine. If it already got there,
          // then you need to complete it yourself. Otherwise, you got
          // through IoCancelIrp before the IRP completed entirely.
          //
          if (InterlockedExchange(&touched, IRPLOCK_CANCEL_COMPLETE) == IRPLOCK_COMPLETED) {
            IoCompleteRequest(irp, IO_NO_INCREMENT);
          }
        }

而改造后的complete过程则如下
  if (InterlockedExchange((PVOID)&touched, IRPLOCK_COMPLETED) == IRPLOCK_CANCEL_STARTED) {
    //
    // Main line code has got the control of the IRP. It will
    // now take the responsibility of completing the IRP.
    // Therefore...
    IoFreeIrp(Irp);
    return STATUS_MORE_PROCESSING_REQUIRED;
  }

简单点说就是在中间加入能表示状态信息的变量touched表征现在所处的状态,cancelable, cancel started, cancel complete, completed四个状态相互协调保证complete rip不会被调用两次。如同在tip 5里提到过的,这也是口耳相传下来的范式,基本上有cancel rip的地方都得这么写。
posted @ 2011-03-28 01:09 gussing 阅读(1011) 评论(0) 编辑

虽然内核开发人员从一开始就要考虑多线程的问题,但用户态开发人员曾经有过一段美好的生活:他们只需关心一条线程(多半是UI线程)并且不必在乎太多性能问题:即使你在主逻辑里嵌套了无数层循环都没关系,该死的摩尔定律替你搞定一切问题。进入多核时代后,用户态开发人员终于发现了他们忽略很久的,但及其重要的一个技术点:多线程。朋友,好生活已经结束了,欢迎你来到混乱的时代。

我知道现在来写这篇东西似乎不合时宜,因为网上已经有无数文章讨论过多线程问题了,各个社区还开发了一个又一个的线程框架帮你解决烦人的琐事,不过我今天的主要目的是为了引出某个内核开发中的棘手问题(就是7.2要讲的,先按下不表),所以各位看官先放小弟一马,让我把旧事拉出来说说完。

说到多线程,最烦人的其实是同步问题。关于这一点我很赞同osr邮件列表里的joe老师的观点:用户态程序不应该出现(自定义的)锁,任何时候你发现自己需要考虑用锁来同步了,就说明你的设计出了问题。同步这摊子事我有一堆话要说,但不是今天,今天我要说另一个比较容易被忽略的点:线程的创建和销毁。许多人不知道如何正确的创建和销毁线程,我看到过无数错误的写法,程序奇迹般的运行正常,但错的就是错的,现在不出问题,不代表以后不会。

创建

线程创建的api是CreateThread,关于这个api只有一条原则需要注意:绝对不要去用它。让我们把时间回退到上实际70年代,那时c语言刚诞生不久,c run time library也才成型,多任务还是个高级玩意儿,如果当时就有咨询公司这种东西,他们甚至可以靠培训多任务相关技术发大财。很自然的,c运行库的作者没有考虑多线程的问题,他们假设整个c语言程序只有一条线程,没有切换,自然也没有重入,所以c运行库里有数不清的全局变量,errno就是最著名的一个。后来多任务出现了,进程和线程的概念也相继登场,这些全局变量就变得棘手了:它们会被重入。仔细推敲我们可以发现,这些全局变量其实不应该是整个地址空间可见的,而应该每个线程一份拷贝才对。实际上现在的c库就是这么干的,微软的msvcr把errno等东西放在TLS(线程本地存储块)中,创建线程的时候分配,销毁线程的时候回收。但是CreateThread作为系统api才不会管这些屁事呢,人家是系统级的,c运行库跟它没关系,问题就出在这里:你敢说你写的程序不用c运行库,所有的工作都用纯api完成?别扯了,还是听话别碰CreateThread为妙。ms vc中有替代函数_beginthreadex/_endthreadex,任何时候都必须用他们创建销毁线程。如果你用的是其他厂商的c库,就用他们提供的线程函数--不管那是什么东西,有多蠢,用就对了--别碰CreateThread。

销毁

没有哪样东西比线程的销毁更恶心人了。如同上面所说,ExitThread函数绝对不能碰,除此之外还需要注意的是:唯一正确的退出方式就是让它跑完所有代码自然退出。但很多时候自然退出根本就是一个奢望,假如你的主线程需要等所有线程退出后才能做下一件事,那么加一个超时时间就是非常必要的,因为你不能让主线程等太久,况且有些线程(特别是IO相关的线程)退不退的出还是个问题。倘若超时事件真的发生,我们就不得不做一件烂事:强制线程退出。这种做法隐患多多,我能想到的大概有以下几个:

1. 资源泄漏。假如线程开始的时候申请了内存,打开了文件,或者其他任何形式的资源,并且在自然退出前释放资源,那么_endthreadexTerminateThread后这些资源就泄漏了,没有人会去回收他们。

2. 锁的状态。假如线程开始的时候获得mutex,自然退出前释放,那么_endthreadexTerminateThread后mutex就进入ABANDONED状态,其他WaitSingleObject的点会返回WAIT_ABANDONED值。仔细想想你的代码有没有处理这个返回值,多半是没有吧…

3. IO相关的问题。如果你的GetOverlapResult调用将Wait参数置为TRUE,那么在IRP被完成之前它是不会返回的,强行退出线程会引起驱动的误会,驱动以为只要Complete了这个IRP,app就会做某类事情,实际上app没做。更糟的是如果GetOverlapResult调用将Wait参数置为FALSE并在随后的代码里进行有超时的等待,等不到就CancelIO,类似这样:

res = GetOverlapResult(…, FALSE);

if( !res )

{

  if( WAIT_TIMEOUT == WaitForSingleObject(overlap.hEvent, 5000))

  {

    CancelIo(m_hDriver);

  }

}

那么强退线程后CancelIo就有可能没被执行到,IRP可能永远都不会被Complete。没有什么比系统中存在一个永远不会complete的IRP更糟糕的事了,你的进程将永远杀不掉,系统的ShutDown过程也会被挂起,恭喜你拔电源把。

4. appverifier会直接crash进程。开发过程中一直挂着appverifier跑是个好习惯,它会把各种隐患暴露给开发人员。比如强退线程这一条,裸奔的程序还能继续执行下去,最终用户不会知道发生了什么事,但挂着appverifier的程序就会爆炸,烧掉你的硬盘,并引发9级地震。好吧我开玩笑的,但你的manager一定不会容忍crash这一点。

5. 如果调用了需要SEH才能实现的功能: raise/signal
这与TLS不同, 少了一个__try块是不能再后面补上的。
这才是比较严重的问题。
但windows下开发, 使用signal的代码确实不多。
(感谢OwnWaterloo提供)

那么到底有没有办法安全的强退线程呢,其实还是有几个的,我能想到的有以下几种:

1. 设置信号通知目标线程退出。比如定义一个BOOL exitThread值,目标线程的大循环就该写成这样

while( !exitThread)

{

  …

}

主线程则是这样:

exitThread = TRUE;

WaitForSingleObject(m_hThread, INFINITE);

这种做法绝大多数情况下有效,但是有race condition,exitThread会被重入。改成这样会更好一点

while ( WaitForSingleObject(hExitEvent,0) != WAIT_TIMEOUT )

{

  …

}

主线程

SetEvent(hExitEvent);

WaitForSingleObject(hThread, INFINITE);

但还是有问题,目标线程的逻辑里如果有调用WaitForSingleObject(…, INFINITE)无限等某事件,那么它还是退不出来。

2. 在主线程里这么干:

SuspendThread(hThread);

GetThreadContext(hThread, &THREADCONTEXT);

THREADCONTEXT.eip = my_exit;

SetThreadContext(hThread, &THREADCONTEXT);

ResumeThread(hThread);

WaitForSingleObject(hThread, INFINITE);

my_exit里释放资源,退出线程。这种做法除了IO相关问题外全都有效,写起来也很有快感,强行修改线程的执行路径,跟在内核里hook system call似的,太酷了,特黑客的感嚼。我对此的建议是:绝对不要这么做。

3. 把目标线程里的WaitForSingleObject换成WaitForSingleObejectEx,把Alertable参数设成TRUE,在主线程里这么干:

QueueUserAPC( UserAPCProc, hThread, 0 );
WaitForSingleObject( hThread, INFINITE ); 

UserAPCProc啥事也不用干,空函数就行。执行完QueueUserAPC函数后,目标线程的WaitForSingleObejectEx函数会立刻被唤醒,并返回WAIT_IO_COMPLETION状态。这是windows核心编程的作者jeffrey大牛道出的天机。这段代码写着也很有快感,居然用到APC耶,我是高手有木有!但是我对此的建议还是:绝对不要这么干。

4. 把目标线程里的WaitForSingleObject换成WaitForMultipleObjects,并在目标线程里这么写:

while ( WaitForSingleObject(hExitEvent,0) != WAIT_TIMEOUT )

{

  …

  HANDLE events[2];

  events[0] = hExitEvent;

  events[1] = hYourAnotherEventThatHaveToWait;

  if( WAIT_OBJECT_0 == WaitForMultipleObjects(2, events, FALSE, INFINITE) ) // you have to exit thread

  {

    …

  }

}

主线程则是这样:

SetEvent(hExitEvent);

WaitForSingleObject(hThread, INFINITE);

这是比较标准的做法,包括IO操作相关的一系列问题都能得到解决。个人建议如果你想让目标线程主动退出,最好采用这个手段,并且确保所有非主要线程里没有WaitForSingleObject这个函数出现。如果想等,就用WaitForMultipleObjects。

到这儿为止该说的差不多说完了,唯一的隐患就在IO那里。正确的退出要求线程从wait函数返回后执行CancelIo操作取消掉你的IRP,这需要驱动配合在IRP里设置CancelRoutine。假如没有CancelRoutine,那么CancelIo操作是失败的,前面讲的那些恐怖故事还是会发生。关于这一点,我打算下次再说。

posted @ 2011-03-27 01:24 gussing 阅读(1413) 评论(6) 编辑

不要再假装自己写的程序没bug了,不可能的,debug工具你早晚得用上。最常见的debug工具非printf(windows上用OutputDebugString函数)莫属,简单方便易学易用,但局限性也是显而易见的,首先它对debugee的影响很大,某些race condition的bug你要多加几个log它就重现不出来了,然后你把log去了发布给客户,结果又成了必现的bug,这种烂事咱们都碰到过,你懂的。其次log能打印的东西有限,有时候你加log追某个变量的值,追到最后发现是其他变量有问题,这时候你又得加log重新跑。最后分析log的过程及其枯燥无聊,而在debug上敲命令分析则充满了乐趣。我知道有些人对debugger持有鄙视的态度,“单步调试是程序员的耻辱”云云。其实我想说的是,有好工具在手上干嘛不用,又不会怀孕,怕什么。

我们今天要来聊聊windbg,windows上debug的神器,(个人感觉)gdb也不如它。不是说windbg本身写的多么多么好,它牛逼的地方在于它是可扩展的,windows上的内核开发人员驱动开发人员用了windbg这么多年,该碰到的问题都碰到过了,该提的需求都提了,该写的扩展也都写了,导致现在的windbg强大无比,基本啥问题都能解决。就算你的需求很奇葩别人没想过,你也可以用windbg提供的开发框架自己写一个扩展,不费什么时间。所以我们看,提供扩展功能的工具都会变成神器,浏览器如此,debug工具也是一样。废话不多说,让我们来看几个非常有用的扩展:

!Drvobj

这个命令接收一个driver的名字作为参数,解析名字并找到对应的driver object,然后把由这个驱动程序产生的device object打印出来,如下:

lkd> !drvobj \Driver\usbhub
Driver object (8999a4f8) is for:
\Driver\usbhub
Driver Extension List: (id , addr)

Device Object list:
89701de8  89a3f330  8974ede8  8977cc98
8985ac98  89862c98  898b4c98  89add030
89876c98  89849aa8

 

我们看到,usbhub驱动程序对应的driver object是8999a4f8,由它生成了10个device object。

!Devobj

这个命令接收device object指针作为参数,并打印出该device object的内容,包括当前处理的irp,refrence count,device extension,以及与这个device object相关的上层驱动和下层驱动等,如下

lkd> !devobj 89701de8 
Device object (89701de8) is for:
USBPDO-9 \Driver\usbhub DriverObject 8999a4f8
Current Irp 00000000 RefCount 0 Type 00000022 Flags 00003040
Dacl e16dcf84 DevExt 89701ea0 DevObjExt 89701fd0 DevNode 89998df0
ExtensionFlags (0000000000) 
AttachedDevice (Upper) 89712b20 \Driver\HidUsb
Device queue is not busy.


lkd> !devobj 89a3f330 
Device object (89a3f330) is for:
USBPDO-8 \Driver\usbhub DriverObject 8999a4f8
Current Irp 00000000 RefCount 0 Type 00000022 Flags 00003040
Dacl e16dcf84 DevExt 89a3f3e8 DevObjExt 89a3f518 DevNode 899b23c8
ExtensionFlags (0000000000) 
AttachedDevice (Upper) 8971cb90 \Driver\TcUsb
Device queue is not busy.

我们查看了usbhub产生的10个device中的前两个,可以看出其中有一个是hidusb,另一个是tcusb。顺着AttachedDevice打印出的内容我们可以手动遍历整个驱动栈,不过这看起来有些麻烦,万幸有人以经写好一个扩展可以帮我们遍历了,那就是

!devstack

该命令也接收device object作为参数,并遍历着把该object以下的驱动栈全部打印出来,直到bus driver为止,如下

lkd> !devstack 89a3f330
  !DevObj   !DrvObj            !DevExt   ObjectName
  8971cb90  \Driver\TcUsb      8971cc48  000000ae
> 89a3f330  \Driver\usbhub     89a3f3e8  USBPDO-8
!DevNode 899b23c8 :
  DeviceInst is "USB\Vid_0483&Pid_2016\5&39a18bdd&0&2"
  ServiceName is "TcUsb"

可以看到tc usb在usbhub之上,而usbhub则是硬件"USB\Vid_0483&Pid_2016\5&39a18bdd&0&2“的bus driver。前面有篇博文我们说到过windows里把几乎所有的资源都抽象成了一个"object”的概念,所有的object都有一个结构一致的object head,以方便提供统一的操作接口,以下命令就是打印出obect信息的命令:

!object

kd> !object \
Object: e1001300  Type: (8a65e2c0) Directory
    ObjectHeader: e10012e8 (old version)
    HandleCount: 0  PointerCount: 39
    Directory Object: 00000000  Name: \
    292 symbolic links snapped through this directory

    Hash Address  Type          Name
    ---- -------  ----          ----
     00  e100b6e0 Directory     ArcName
         8a515030 Device        Ntfs
     01  e2726b88 Port          SeLsaCommandPort
     03  e1011488 Key           \REGISTRY
     05  e2728888 Port          ThemeApiPort
     06  e16c7f68 Port          XactSrvLpcPort
     09  e1d4d428 Directory     NLS
     10  e1001078 SymbolicLink  DosDevices
     13  e1c9e160 Port          SeRmCommandPort
     14  8a577030 Device        Dfs…

我们查看了根目录,并列出了它的所有子项(太多了,没全贴上来),它的功能跟winobj很像,不过没有winobj直观。再来看几个跟power有关的命令。我们知道用wdm写驱动最麻烦的事情之一就是所有的power命令都要自己handle,而wdf则帮我们全包圆了(又回到上会的讨论了不是),没包圆也有它的好处,就是你得强迫自己去理解这部分内容。power irp处理不好,机器很容易就不能进s3/s4或者不能从s3/s4唤醒,这时候我们就得借助windbg来追查问题到底出在哪儿,查看当前power状态的命令是

!podev

该命令接收device object为参数,打印它当前的power状态

lkd> !podev 89784b10 
Device object is for:
  DriverObject 899d4410
Current Irp 00000000 RefCount 0 Type 00000002 DevFlags 00000050
Device queue is not busy.
Device Object Extension: 89784bc8:
PowerFlags: 00000000 =>SystemState=0 DeviceState=0
Dope: 00000000:

我们看到目前device处于d0(working)状态,系统处于s0(idle)状态。但是这个命令只能给我们一个总结,到底哪些power irp正在处理我们没法看出来。以下命令正是列出系统中所有power irp的

!poreqlist

lkd> !poreqlist
All active Power Irps from PoRequestPowerIrp
PopReqestedPowerIrpList
FieldOffset = 00000004
Irp 8a60ba20 DevObj 8a5c1d70 \Driver\ACPI Ctx 00000004   Wait Wake S3
Irp 882f1e00 DevObj 89a05440 \Driver\usbuhci Ctx 00000001   Wait Wake S0
Irp 883dee00 DevObj 89a2b218 \Driver\usbuhci Ctx 00000001   Wait Wake S0
Irp 8843c008 DevObj 89a20528 \Driver\usbuhci Ctx 00000001   Wait Wake S0
Irp 884ca220 DevObj 89a10030 \Driver\usbehci Ctx 00000001   Wait Wake S0
Irp 883662d0 DevObj 89b36030 \Driver\usbehci Ctx 00000001   Wait Wake S0
Irp 87ec2008 DevObj 8974ede8 \Driver\usbhub Ctx 00000001   Wait Wake S0
Irp 89b4ec00 DevObj 899b7618 \Driver\usbuhci Ctx 00000001   Wait Wake S0
Irp 882ca7d0 DevObj 89a3f330 \Driver\usbhub Ctx 00000001   Wait Wake S0

我们看到很多的hid设备处于等待唤醒的状态。

最后我们来聊聊内核调试时如何调试用户态的东西。我们知道在进程的用户态部分相互隔离,而内核部分都是share同一个地址空间,由这特性带来的好处坏处我们先不谈,今天先关注具体问题。断在kernel里的debugger要调试别的进程的kernel部分很容易,因为地址是同一块,但是要调试user mode部分就不那么容易了。我们知道user mode是不可能直接访问内核地址的,cpu在将虚地址翻译成物理地址的时候会检查特权级,user mode是第3级而内核是第0级,倘若第三级的指令带的地址是第0级,cpu会抛拒绝访问的异常。反过来,内核指令访问user mode地址虽然可行,不过得考虑进程上下文,如果你不管进程上下文直接访问user mode地址,有两种错误情况会发生:你访问的根本不是你想要的进程,或者你访问的地址根本没有东西。地址空间分为用户态和内核态这种说法是站在用户态角度讲的,它假设所有的线程都有"用户态"部分,实际上PsCreateSystemThread产生的内核线程是没有用户态部分的,它附在一个叫"System”的进程上,而"System”进程只是为管理方便而存在的虚拟的东西,没有实体。所以我们说从内核debugger调试用户态内容远没有kernel调kernel,user调user那么简单,你必须关心进程上下文这个东西。下面这个命令可以列出系统中所有的进程:

!process

lkd> !process  -1 0
PROCESS 8854b020  SessionId: 0  Cid: 1060    Peb: 7ffd4000  ParentCid: 04b4
    DirBase: 0abc0ae0  ObjectTable: e12e5650  HandleCount: 396.
    Image: windbg.exe

lkd> !process  0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 881de6f8  SessionId: 0  Cid: 1498    Peb: 7ffde000  ParentCid: 04b4
    DirBase: 0abc0c20  ObjectTable: e465aa70  HandleCount:  46.
    Image: notepad.exe

如上所示,当前进程是windbg,process object为8854b020,而我们关心notepad进程的process object是881de6f8 ,并不是同一个,我们首先要做的事情就是切换到notepad这个进程上去,使用内置命令.process

lkd> .process 881de6f8
Implicit process is now 881de6f8

然后f5让系统跑一会儿(windbg断住的时候,整个系统是挂起的,进程切换也不会发生),再次断下来的时候你就已经在notepad进程里了(上面列的打印都是在local kernel debug里的,功能很受限,比如系统没挂起,f5也不能用。接下来我要切换到双机调试模式)

kd> !process -1 0
PROCESS 863c22f0 SessionId: 0 Cid: 00bc Peb: 7ffdb000 ParentCid: 05fc
DirBase: 06c602c0 ObjectTable: e16718e8 HandleCount: 29.
Image: notepad.exe

虽然已经在notepad进程里了,但user mode的东西依然不可见,因为windbg会缓存用户态的信息,进程切换后,你得手工刷新缓存,用内置命令.reload /user。做完这一步后,user mode的信息就变得可见了,你可以在用户态函数里下断点:kd> bp /p @$proc ntdll!ntcreatefile,或者列出加载模块:lm 等等,就跟普通的用户态程序调试一摸一样。上面那一套步骤很烦,却是步步都不能省,有没有方法简化它呢?如开头所说,windbg是可扩展的,你想到的需求,别人早就想到,并且已经写好工具等你用了,以下命令做完一整套动作:

!bpid

该命令接收process cid作为参数,找到对应的进程并切换进程上下文,跑一会儿,断下来,刷新用户态内容,全部搞定。怎么样,是不是很方便?

posted @ 2011-01-24 15:12 gussing 阅读(1270) 评论(0) 编辑
摘要: IO_STACK_LOCATION很重要,再多聊一点也无妨。上上回我们谈了IO_STACK_LOCATION和那几个重要的函数,当然,我的目的不是扫盲,而是记下一些容易犯错的地方(实际上都是工作中碰到过的钉子)以方便自己回顾。我的记性是如此的差以至于几月不看就会忘记。如果你对这东西没概念,我建议你先多查查WDK文档。上回我们聊了IoCopyCurrentIrpStackLocationToNext和IoSkipCurrentIrpStackLocation的差别(你看我的记性是不是很差,其实是上上回说的),结果把要聊的核心内容给忘了。IO_STACK_LOCATION这坨东西出现的原因很大程度阅读全文
posted @ 2011-01-23 01:38 gussing 阅读(1132) 评论(2) 编辑
摘要: 今天我们聊一聊CreateFile,这个名字取的不合适但IO的世界里完全绕不过去的东西,以及与之相关的“namespace”这一概念。我们知道Create的意思是创造,创建,上帝创造了这个世界,指的可不是上帝打开了某样存在的东西(唯物主义者,我知道你们有意见,给我闭嘴…),但这个倒霉的函数要做的却是打开。我们也知道File是文件,windows里面也没有“一切都是文件”的概念,但这个倒霉的函数要做的却是打开所有能返回handle的内核对象。Anyway,CreateFile函数是唯一一个能打开内核对象的handle,并让user mode app来访问的方法。将范围缩小到驱动,这个函数也是唯一阅读全文
posted @ 2011-01-21 14:34 gussing 阅读(1508) 评论(6) 编辑
摘要: 今天我们来聊聊IRQL,这是驱动新手的梦魇,想想看多少BSOD是因为IRQL不对引起的。这也是*NIX类内核开发人员最喜欢的吐槽点之一,你看linux里就没有这个概念,我们还不是活的好好的?我偶尔有时候能得着一些空,也会问一样的问题:为毛?为毛要有这东西存在!后来我想通了。我们先聊passive level和interrupt level。passive level是普通级别,同时也是优先级最低的,所有的用户态线程和大部分的内核态线程都会在这个级别上运行。interrupt level则是中断服务例程的运行级别。这两者有差很好理解,几乎所有os教程里都有告诫我们中断服务例程要尽可能快的完成,并阅读全文
posted @ 2011-01-20 14:39 gussing 阅读(1106) 评论(0) 编辑
摘要: 如前文所述,nt内核的驱动模型没有完全使用函数调用栈,而是自己山寨出来一个IO_STACK_LOCATION,里面保存了驱动调用序列。我们知道函数调用栈的push和pop都是编译器帮忙弄的,你甚至都可以在完全不了解内幕的前提下写代码,但是驱动开发不一样,调用序列要你自己去关心,何时入栈,何时出栈,栈内保留的什么内容,全部都要照顾好,否则BSOD就在前方不远等你。与IO_STACK_LOCATION有关的函数有以下几个:IoSkipCurrentIrpStackLocation, IoSetNextIrpStackLocation, IoGetNextIrpStackLocation, IoCo阅读全文
posted @ 2011-01-19 13:54 gussing 阅读(1325) 评论(0) 编辑
摘要: nt内核的IO模型中,IRP有两类:threaded irp和non-threaded irp,顾名思义,前者跟thread绑定,后者跟thread无关。当一个threaded irp被创建时,创建线程会有一个队列保存该irp,直到irp完成之后才释放。当你试图让这条线程退出时,系统会检测队列看里面是否还有irp没完成,如果有,线程会一直等待,直到所有的irp全部完成。而non-thread irp则正好相反,如果该irp已经返回到了创建它的地方你还继续complete它,BSOD将会发生。Threaded IRP如前面所讲,threaded irp和线程绑定在一起。当user mode程序发阅读全文
posted @ 2011-01-18 11:32 gussing 阅读(1183) 评论(0) 编辑