原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
本文主要参考了实验楼中"使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用"的实验.
部分则讲解系统调用的处理过程.
系统调用是操作系统中耳熟能详的一个名词了, 本科上OS时的记忆已经有点模糊, 乘着这次作业刚好可以回顾一下.
了解系统调用之前, 先来看看中断.
中断通常分为两类: 同步(synchronous)中断和异步(asynchronous)中断, 顾名思义, 前者是在一条指令中止执行后CPU才会发出的; 后者则是有其他硬件设备按照CPU时钟信号随机产生的. 我们也常常成同步中断和异步中断为异常(exception)和中断(interrupt).
Intel的文档中把中断和异常分为以下几个大类:
编程异常通常是由我们这些程序猿引发的, 众所周知, 在linux系统中, 凡是涉及到资源管理和设备交互这些事情, 通常是由OS管理, 当我们需要调用系统的功能时, 通常通过系统调用将这些服务交由OS来做. 系统调用就是OS为用户态进程与硬件设备进行交互提供的一组接口, 系统调用的好处多多, 不仅把用户(client, 编程用户: 程序猿)从底层的硬件编程中解放出来, 还极大地提高了系统的安全性, 使得我们开发的程序有了可移植性.
关于以上的更多详细介绍, 可以参考维基百科: 中断
系统调用的缘由和原理我们知道了, 但是如何实现系统调用, 才是认识过程的第二次飞跃. Linux内核的底层代码都是通过C语言实现的, 在C语言的库中, 就有一些API引用了封装例程, 封装例程的目的, 就是进行系统调用. 我们常见的printf(), chdir() 其实都属于系统调用的范畴.
结合前面两个标题, 我们对于中断, 系统调用有了泛泛的了解, 更深入一点, 系统调用的过程中, 究竟发生了什么?
我们知道, 在进行中断的时候, 通常系统会发出中断号, 中断号的目的, 就是为了查找中断服务程序的地址, 通常中断号 ∗ * ∗ 4 即为中断向量的地址, 而中断向量通过中断描述表( I n t e r r u p t d e s c r i p t i o n t a b l e , I D T Interrupt description table, IDT Interruptdescriptiontable,IDT)则可以找到相应的中断服务程序.
系统调用既然属于中断的一种, 在执行系统调用时, 必然也会有一个中断号, 那便是128号, 系统调用有那么多种, 仅仅一个处理程序怎么应付的过来? 系统调用号就起到了作用, 其类似于中断号, 也是在一个系统调用分派表(dispatch table)中对需要的系统调用子例程查找, 完成系统调用.
下面具体介绍本次实验.
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar.xz
tar -xvf linux-5.0.1.tar
cd linux-5.0.1
sudo apt install build-essential flex bison libssl-dev libelf-dev libncurses-dev
make i386_defconfig
make -j4
我在这里采用的是32位的缺省设置, 由于编译过程默认采用单核, 这里用-j4参数使其并行编译, 大概用了5分钟左右.编译结果:
sudo apt install qemu
qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage
git clone https://github.com/mengning/menu.git
cd menu
sudo apt-get install libc6-dev-i386 # 在64位环境下编译32位需安装
make rootfs
cd ..
qemu-system-i386 -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd
rootfs.img
#include
#include
int main()
{
char *buffer;
buffer = getcwd(NULL, 0);
printf("Current dir is: %s\n", buffer);
chdir("/tmp");
buffer = getcwd(NULL, 0);
printf("Current dir is: %s\n", buffer);
}
#include
#include
int main()
{
char *path = "/";
char *pwd;
int flag;
asm volatile(
"movl %1, %%edi\n\t"
"movl $0x0c, %%eax"
"int $0x80"
"movl %%eax, %0\n\t"
:"=m"(flag)
:"m" (path)
);
pwd = getcwd(NULL, 0);
printf("%d\n %s\n", flag, pwd);
return 0;
}
这里我作弊一下, 我先将.C文件编译成.S汇编文件, 略微分析一下再写的答案.
在上面的API编程中, chdir是有参数的, 而在嵌入式汇编中, 要将参数传递给函数, 必须先将该参数传递给寄存器, 由系统调用服务例程运行时取出该参数. 故在这里我们将path传递给edi寄存器. chdir在系统调用中的调用号为12号, 故将12的16进制传递给eax寄存器, 这里也需要解释一下, eax寄存器在系统调用时, 通常作为系统调用号的暂存器传递给系统调用服务例程. int 0x80就不用解释了. 接下来将返回值赋值给flag变量, 若chdir 成功, 则flag应该为1, 否则为0, 然后输出当前的工作目录. 如下图: