本周的实验主要是通过gdb工具来调试查看Linux x86的内核代码,首先需要对gdb有一定的了解:
GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像VC、BCB等IDE的调试,但如果你是在UNIX平台下做软件,你会发现GDB这个调试工具有比VC、BCB的图形化调试器更强大的功能。所谓“寸有所长,尺有所短”就是这个道理。
一般来说,GDB主要帮忙你完成下面四个方面的功能:
1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
3、当程序被停住时,可以检查此时你的程序中所发生的事。
4、动态的改变你程序的执行环境。
实验的第一部是打开shell执行如下命令:
cd LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
可以打开MenuOS系统
输入help命令,可以看到系统支持三个简单命令:help、version、quit
我们再另外开一个shell窗口 进入LinuxKernel目录,输入如下命令:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s –S
其中
# -S freeze CPU at startup (use ’c’ tostart execution) 在系统启动的时候冻结CPU,使用c键继续执行后续操作
# -s shorthand for -gdb tcp::1234 打开远程调试端口,若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
执行结果如下
再打开另一个终端
执行gdb命令:
file linux-3.18.6/vmlinux
target remote:1234
break start_kernel
c
既可以在start_kernel处设置断点
系统执行到start_kernel()函数,输入list命令可以查看之后的代码
对于start_kernel()函数
/* * Need to run as early as possible, to initialize the * lockdep hash: */ lockdep_init();//lockdep是一个内核调试模块,用来检查内核互斥机制(尤其是自旋锁)潜在的死锁问题。 set_task_stack_end_magic(&init_task);//手工创建的PCB,0号进程即最终的idle进程。 /* * Set up the the initial canary ASAP: */ boot_init_stack_canary();//canary值的是用于防止栈溢出攻击的堆栈的保护字。 trap_init();//对内核陷阱异常进行初始化。 mm_init();//初始化内核内存分配器。 /* * Set up the scheduler prior starting any interrupts (such as the * timer interrupt). Full topology setup happens at smp_init() * time - but meanwhile we still have a functioning scheduler. */ sched_init();//初始化调度器数据结构,并创建运行队列。 /* Do the rest non-__init'ed, we're now alive */ rest_init();//start_kernel()函数中调用的最后一个函数。
对于rest_init()函数
int pid;//定义pid变量存放进程号 rcu_scheduler_starting(); RCU(Read-Copy Update)//锁机制启动。 /* * We need to spawn init first so that it obtains pid 1, however * the init task will end up wanting to create kthreads, which, if * we schedule it before we create kthreadd, will OOPS. */ kernel_thread(kernel_init, NULL, CLONE_FS);//第一个用户态进程kernel_init numa_default_policy(); pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);//创建kthreadd内核线程,它的作用是管理和调度其它内核线程。 rcu_read_lock(); kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);//获取kthreadd的线程信息,获取完成说明kthreadd已经创建成功。 rcu_read_unlock(); complete(&kthreadd_done);//通过一个complete变量(kthreadd_done)来通知kernel_init线程。
对于qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img 指令 qemu是一个快速的动态译指的虚拟机。他支持多种处理器指令集的模拟。bzImage 压缩的内核映像 initrd 是在系统引导过程中挂载的一个临时根文件系统就是rootfs.img
对于file linux-3.18.6/vmlinux vmlinux 它是是未压缩的内核,即编译出来的最原始的文件用于kernel调试,产生system.map符号表,不能用于直接加载 调试的程序用file打开
对于kernel_thread()函数 int kernel_thread (int ( * fn )( void * ), void * arg, unsigned long flags); 1. 内核线程是通过系统调用clone()来实现的,使用CLONE_VM标志(用户还可以 提供其他标志,CLONE_PID,CLONE_FS,CLONE_FILES等),因此内核线程与调用 的进程(current)具有相同的进程空间.2. 由于调用进程是在内核里调用kernel_thread(),因此当系统调用返回时,子进程也处于 内核态中,而子进程随后调用fn,当fn退出时,子进程调用exit()退出,所以子进程是在 内核态运行的. 3. 由于内核线程是在内核态运行的,因此内核线程可以访问内核中数据,调用内核函数. 运行过程中不能被抢占.
Linux在无进程概念的情况下将一直从初始化部分的代码执行到start_kernel,然后再到其最后一个函数调用rest_init。从rest_init开始,Linux开始产生进程,因为init_task是人为制造出来的其pid=0,所以它并不能称之为一个真正的进程。在rest_init函数中,内核将产生第一个真正的进程pid=1.在这之后所有的用户态进程的祖先都应该是rest_init产生的1号进程.在初始化的最后内核再调用scheule()就能使得整个系统运转起来了.对于schedule(),内核必须知道什么时候才能调度schedule(),内核提供了一个need_reched()标志来标明是否需要重新执行一次调度.对于Linux的调度策略有这几个特点1高效性:高效意味着在相同的时间下要完成更多的任务。调度程序会被频繁的执行,所以调度程序要尽可能的高效;2.加强交互性能:在系统相当的负载下,也要保证系统的响应时间;3.保证公平和避免饥渴;4.SMP调度:调度程序必须支持多处理系统;5.软实时调度:系统必须有效的调用实时进程,但不保证一定满足其要求.