您的位置:首页 > 运维架构 > Linux

Linux内核分析学习笔记:system_call中断处理过程

2016-03-27 17:33 423 查看
原创内容(陈晓爽 cxsmarkchan)

转载请注明出处

《Linux内核分析》MOOC课程 学习笔记

前两篇博文从汇编的角度分析了linux系统的系统调用方法,本博客在实验楼平台下写了一个简单的系统调用程序,并分析系统调用的实际过程。

1 实验内容

本文实验平台为实验楼Linux内核分析的第5个实验:分析system_call中断处理过程。

在实验平台下,切换到
~/LinuxKernel/menu
文件夹下。该文件夹下的内容会被写入磁盘镜像
rootfs.img
中。Linux加载磁盘后,启动的第一个用户态进程(可参考linux内核分析学习笔记:用gdb跟踪linux内核启动过程)即在该文件夹的test.c中。

运行
make rootfs
,可以编译并启动系统,结果如下:



该系统中仅有3个命令,分别是
help
,
version
,
quit
。下面在该系统中添加两个命令:
write
write-asm
。顾名思义,这两个函数是系统调用
sys_write
的C语言版本和汇编版本。

打开
test.c
,可以看到main函数如下:

int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","XXX V1.0(Menu program v1.0 inside)",NULL);
MenuConfig("quit","Quit from XXX",Quit);

ExecuteMenu();
}


可见main函数中给出了命令和对应函数。此处不再关心
SetPrompt
ExecuteMenu
函数的细节(与Linux操作系统无关),只需了解
MenuConfig
函数的定义如下:

int MenuConfig(char *cmd, char *desc, int (*handler)());


其中,cmd为命令名称,desc为命令说明(在help中显示),handler是命令处理程序。

将main函数改写如下:

int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","XXX V1.0(Menu program v1.0 inside)",NULL);
MenuConfig("quit","Quit from XXX",Quit);
MenuConfig("write", "Print to screen", Write);
MenuConfig("write-asm", "Print to screen(asm)", Write_asm);

ExecuteMenu();
}


这样就添加了两条命令。接下来,给出
Write
Write_asm
函数:

int Write(int argc, char *argv[]){
int i;
for(i = 1; i < argc; i++){
write(0, argv[i], strlen(argv[i]));
write(0, "\n", 1);
}
return 0;
}

void Write_asm(int argc, char *argv[]){
int i;
int len;
char *str;
for(i = 1; i < argc; i++){
len = strlen(argv[i]);
str = argv[i];
asm volatile(
"int $0x80\n\t"
"mov $4, %%eax\n\t"
"mov $0, %%ebx\n\t"
"mov %4, %%ecx\n\t"
"mov $1, %%edx\n\t"
"int $0x80\n\t"
:
:"a"(4), "b"(0), "c"(str), "d"(len), "D"("\n")
);
}
return 0;
}


这段代码也不再解释,可以参照前一篇博文用asm内联汇编实现系统调用

添加了如下代码后,运行系统,点击help命令,就会出现更多的选择。同时,write命令和write-asm命令都可以执行,其功能是将参数表中的所有内容按顺序输出,并自动添加换行。

运行效果如下图:



2 系统调用的过程

下面分析系统调用的过程。在汇编代码中,我们通过
int $0x80
产生中断,系统会在中断向量表中查找相应的中断处理函数。
0x80
对应的函数即为系统调用函数
system_call
,位于
arch/x86/kernel/entry_32.S


遗憾的是,entry_32.S是一段汇编代码,system_call也不是一个函数,只是一个程序入口标志。因此,无法用gdb来对代码进行单步调试。因此,我们仅通过汇编代码的分析来了解系统调用的过程。

系统调用的汇编代码可以简化如下,其中省略了部分代码:

ENTRY(system_call)
#……
SAVE_ALL
#……
syscall_call:
call *sys_call_table(,%eax,4)
syscall_after_call:
movl %eax,PT_EAX(%esp)      # store the return value
syscall_exit:
#……
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work

restore_all:
TRACE_IRQS_IRET
#……
irq_return:
INTERRUPT_RETURN

ENDPROC(system_call)


在该段代码执行时,系统已经进入内核态。该段代码首先通过
SAVE_ALL
宏保存现场,接下来根据
eax
寄存器的内容,查找
sys_call_table
表,跳转到合适的系统调用,并把返回值存入
eax
寄存器中。完成这一步之后,系统调用就基本结束了,会执行restore_all部分的代码恢复现场,最后执行
iret
(此处即为
INTERRUPT_RETURN
)返回到中断调用的位置,继续执行。

值得注意的是,在返回之前,系统还会根据情况,决定是否要调用
syscall_exit_work
函数。该函数的意义在于退出系统调用前,执行一些清理工作。其中最重要的工作是进程调度,这是因为有些系统调用函数可能会改变进程执行情况,例如挂起当前进程。此时,在该函数中就会调用进程调度函数,实现进程切换。

3 小结

系统调用处理过程本质是一种中断过程,因此遵循中断处理的一些共性,包括:

根据中断号和中断向量表,跳转到相应的中断处理程序;

在中断开始的时候保存中断现场,在中断结束的时候恢复中断现场;

中断结束时通过iret返回到中断发生前的程序执行位置;

同时,在系统调用的过程中,还会根据
eax
寄存器中存储的系统调用号,进一步查找系统调用表,执行系统调用操作。系统调用结束后,程序会根据系统调用的结果,进行一些收尾工作,例如进程调度等,最后退出内核态,返回用户态。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: