Wednesday, January 27, 2010

QEMU小实验:手工遍历所有进程的方法

QEMU小实验:手工遍历所有进程的方法

在内核中已经提供了遍历所有进程的方法,比如用for_each_process宏。但是如果你想加深对这部分的了解,那么可以不用这个宏,完全手工遍历一遍。下面介绍了在QEMU中,利用QEMU MONITOR,手工找出所有PROCESS的方法

环境:
QEMU 0.9.1
QEMU VM: CENT OS 5.3 (LINUX 2.6.18)
ARCH :X86-32

主要思路:
我们已经知道LINUX中的所有进程都对应一个TASK_STRUCT. 同时这些TASK_STRUCT里面有一个成员叫做TASKS。它的类型是一个LIST_HEAD(有NEXT, PRE2个成员,构成一个双向链表)。所有的进程就是通过这个TASK_STRUCT里的TASKS连接在一起的。

想遍历进程的话,可以找到一个上面说的结构,然后顺着链表一个一个找下去。另外,还有一些重要的信息在TASK_STRUCT中。常用的有PID(线程ID), TGID(进程ID), COMM(进程的命令参数)。
(关于PID, TGID,可以看下面这个帖子:http://linux.chinaunix.net/bbs/thread-1155667-1-1.html

如果是在GDB里,那么找到了TASK_STRUCT之后,可以直接用 p task->pid 的方法来把PID之类的打印出来。但是我们的目的就是不用GDB的功能,而只用QEMU MONITOR的功能,纯手工的找出PID这些成员。

这样一来,我们需要自己计算下PID, TGID等成员在TASK_STRUCT中的偏移量。怎么算呢?最天真的办法是按照 .H 文件里TASK_STRUCT的声明,自己一个一个算过去,比如一个CHAR占一个字节,一个INT 4个字节等。但是由于TASK_STRUCT是一个很大的结构(包括几十或者上百个成员),同时考虑到编译的时候还有对齐的问题。这样手工算可以说既费力又不一定对。

最厉害的办法是自己写个类似的C编译器,直接把编译后的输出打印出来。感兴趣的可以看看CIL(C Intermediate Language)。但是CIL还是有点麻烦的。它是用OCAML语言写的。我不是很熟悉。

于是我采用了一个比较折中的办法:自己写个KERNEL MODULE, 在这个MODULE里直接定义一个TASK_STRUCT,然后把感兴趣的那些成员的地址和基地址都打印出来。或者直接把偏移量打印出来。

我就是用上述办法得到了几个关键成员的偏移量:
(前面是TASK_STRUCT里的成员,后面是它距离TASK_STRUCT基地址的偏移量)
pid = 0Xbc
tgid = 0Xc0
comm = 0X1AC
tasks = 0X80

还有一点,我们怎么找到第一个TASK_STRUCT呢?可以看这篇文章(如何找出CURRENT):
http://linux.chinaunix.net/bbs/viewthread.php?tid=1147973&extra=

好了,万事具备,开始行动吧。
1) 运行一个QEMU VM。我是在WINDOWS HOST上跑的QEMU. 在LINUX上也可以跑QEMU。但是LINUX上的QEMU有一点不好,那就是它的QEMU MONITOR的大小是固定的。太小了。而在WINDOWS上面,QEMU MONITOR可以变得很大。

2) 按ALT+CTRL+2, 切换到QEMU MONITOR. 输入STOP. 这样QEMU 就会暂停下来了。这样的好处是你可以花很多时间来遍历进程。而不用担心某个进程结束后,带来的地址无效的问题。

3) p $esp :找出当前的ESP。我实验的时的输出是 0xc3a94f78

4) 找出THREAD_INFO:也就是把ESP与上0XFFFFE000。得到0xc3a94000

5) x /20w 0xc3a94000。显示的这个内容的第一个指针(前4个字节)就是当前进程的TASK_STRUCT。因为THREAD_INFO里的第一个成员就是struct task_struct *task
我这里得到的结果是0xc3aaa370。

(注意:有的时候这个地方显示的数据全都是0。我怀疑是因为进程正在切换中,内核栈刚刚清空。碰到这种情况,可以在QEMU MONITOR里输入c, 让QEMU继续跑一段时间,然后输入stop. 从头开始。有时要多试几次)

6) 找到了TASK_STRUCT之后,根据前面找到的偏移量,可以方便的找到PID, TGID, COMM, TASKS 等成员的值。比如PID的位置在0xc3aaa42c (TASK+0XBC)。
注意,在看COMM的时候,要用下面这个命令: x /20b 0xc3aaa51c. 否则的话,QEMU会把数据当成DWORD处理,并自动转换Endian. 使得看到的字符串的顺序是反过来的。

7) 这样一来,当前进程的信息已经知道了 。在我的输出中,PID,TGID=0,COMM=SWAPPER. 我们开始找下一个。下一个进程是通过TASK_STRUCT->TASKS 连接在一起的。有一点要注意,TASK_STRUCT->TASKS->NEXT中的地址不是下个TASK_STRUCT的起始地址,而是下个TASK_STRUCT中的TASKS的地址。所以有了这个地址后,要先减去0X80才得到基地址。然后就可以用上面的方法找出PID, TGID, COMM来了。

No comments: