学号最后三位编号:008
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
使用Ubuntu编译Linux内核5.0
- 从官网下载最新的Linux内核源码
- 将刚刚下载好的内核文件解压到 /usr/src 目录下
sudo tar -xvf linux-5.0.1.tar.xz -C /usr/src/
- 编译内核的过程中可能需要安装的依赖库
sudo apt-get install libncurses5-dev libssl-dev sudo apt-get install build-essential openssl sudo apt-get install zlibc minizip sudo apt-get install libidn11-dev libidn11 sudo apt-get install bison sudo apt-get install flex
- 配置内核编译参数(后面需要使用gdb跟踪调试内核)
sudo make menuconfig
- 编译内核
make
(时间较长)
编译最后阶段的输出信息如下所示:
准备根文件系统
mkdir ~/LinuxKernel
cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git
cd menu/
gcc -o init linktable.c menu.c test.c -m32 -static -pthread
cd ../rootfs/
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
由于使用gcc在64位机器上编译32位程序,加上-m32编译选项,报了如下错误:
解决方法是安装如下包即可:
sudo apt-get install libc6-dev-i386
运行 MenuOS:
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu // 建立一条软链接,使用qemu指令来替换qemu-system-i386指令
qemu -kernel /usr/src/linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img // 启动qemu
使用gdb跟踪调试内核
qemu -kernel /usr/src/linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S -append nokaslr // 需要加上“-append nokaslr”这个选项,否则设置的断点不起作用
另外打开一个shell窗口
cd /usr/src/linux-5.0.1/
gdb
(gdb) file vmlinux // 在gdb界面中targe remote之前加载符号表
(gdb) target remote:1234 // 建立gdb和gdbserver之间的连接,按 c 让qemu上的Linux继续运行
(gdb) break start_kernel // 断点的设置可以在target remote之前,也可以在之后
start_kernel函数作为内核的入口函数,定义在init/main.c文件中。它主要是初始化系统相关的内容,以便系统进入一种服务状态,提供各种API调用的服务。至此,使用Ubuntu编译Linux内核5.0以及使用gdb跟踪调试内核基本成功。
选择系统调用进行跟踪分析
根据学号后两位08,查表可知选择的系统调用为creat。
函数名称 | creat |
---|---|
头文件 | #include <sys/types.h> #include <fcntl.h> #include <sys/stat.h> |
函数原型 | int creat(const char *path, mode_t mode) |
函数功能 | 创建文件,显然参数只需要(路径path,权限mode)。 |
参数说明 | filename:要创建的文件名(包含路径,缺省为当前路径) mode:创建模式(可以用数字来表示访问权限, 可读、可写、可执行 7) |
函数返回值 | creat的返回值为文件描述符,由操作系统进行分配 |
进入menu文件夹,编辑test.c文件:
cd ~/LinuxKernel/menu/
sudo vim test.c
- 使用库函数API方式触发系统调用creat的代码如下所示:
- 使用C代码中嵌入汇编方式触发系统调用creat的代码如下所示:
这里使用的嵌入式汇编格式如下所示:
- 给qemu增加两个有关使用creat系统调用的菜单命令,代码如下所示:
按照如上的操作步骤,重新编译Linux内核,并且重新准备根文件系统,如下所示:
运行MenuOS,如下所示:gcc -o init linktable.c menu.c test.c -m32 -static -pthread cd ../rootfs/ cp ../menu/init ./ find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
qemu -kernel /usr/src/linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img
使用GDB调试,跟踪creat系统调用
总结
系统调用(system call)
- 操作系统为用户态运行的进程和硬件设备之间进行交互提供了一组接口,编程人员通过向内核发出系统调用,向操作系统提出服务请求,由操作系统代为完成。
- 系统调用是用户态进入内核态的唯一接口,本身并非内核函数,但是由内核函数实现。进入内核之后,不同的系统调用会找到各自对应的内核处理函数,这些内核函数称为系统调用的“服务例程”。例如系统调用creat实际调用的服务例程为sys_creat(),或者说系统调用creat是服务例程sys_creat()的封装例程。
- 使用系统调用的优点是:首先使得编程更加容易,把用户从学习硬件设备的低级编程特性之中解放了出来;其次,极大地提高了系统的安全性,使得内核在满足用户的某个服务请求之前,能够事先检查用户请求的准确性。
- 保护现场就是进入中断服务程序之前,保存需要用到的寄存器数据; 恢复现场就是进入中断服务程序之前,恢复之前保存的寄存器数据。
- 应用编程接口(API)只是一个函数定义,说明了如何获得一个给定的服务;而系统调用是通过软中断向内核态发出一个明确的服务请求。通常情况下,每个系统调用对应一个封装例程,而封装例程定义了应用程序所使用的API。
Int $0x80指令
- Linux中实现系统调用利用了i386体系结构中的软件中断,即通过
int $0x80
这条汇编指令来发出系统调用。这条汇编指令将用户态的执行模式转变为内核态,并且把控制权交给系统调用处理程序的入口:system_call()处理函数。
system_call()函数
- 首先将系统调用号(eax)和可能用到的所有CPU寄存器保存到相应的堆栈中(由SAVE_ALL来完成)。
- 对用户态进程传递过来的系统调用号进行有效性检查(eax是系统调用号)。
- 如果是合法的系统调用,根据eax中的系统调用号,内核进程查看系统调用表(sys_call_table),并且找到相应的服务例程。
- 服务例程结束以后,从eax获得系统调用的返回值,并且把这个返回值存放在曾经保存用户态eax寄存器栈单元的那个位置上。
- 跳转到ret_from_sys_call(),终止系统调用程序的执行。
- 当进程恢复它在用户态的执行之前,RESTORE_ALL宏定义会恢复用户进入内核前被保存到堆栈中的寄存器的值。
系统调用的参数传递
- 系统调用使用寄存器来传递参数,要传递的参数有:系统调用号、系统调用所需要的参数信息。
- eax寄存器用于保存系统调用号以及系统调用的返回值。
- 系统调用的参数依次保存在:ebx、ecx、edx、esi和edi寄存器中。
- 进入内核态之后,system_call()函数通过使用宏定义SAVE_ALL把这些寄存器的值保存在内核态的堆栈中。
- 参数的个数不能够超过6个(包括eax寄存器中传递的系统调用号);否则,需要使用一个单独的寄存器指向进程地址空间中这些参数值所在的一个内存区域。
有关creat系统调用的分析
- 程序中调用库的封装函数creat,该封装例程将系统调用号0x08压入eax寄存器中。
- 调用软中断 Int $0x80汇编指令进入内核。
- 在内核态中首先执行system_call() 函数,接着根据系统调用号在系统调用表(sys_call_table)中找到对应的系统调用服务例程 sys_creat()。
- 执行系统调用服务例程sys_creat()。
- 执行完毕后,跳转到ret_from_sys_call(),终止系统调用程序的执行,从系统调用返回到用户态。