通过系统调用分析system_call中断处理过程
Posted eyoulc123
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过系统调用分析system_call中断处理过程相关的知识,希望对你有一定的参考价值。
罗冲 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1. 实验准备
1.1 环境准备
下载linux3.18.6的源代码。 按照http://mooc.study.163.com/learn/USTC-1000029000?tid=2001214000#/learn/content?type=detail&id=2001400011给出步骤进行编译
# 下载内核源代码编译内核
cd ~/LinuxKernel/
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
xz -d linux-3.18.6.tar.xz
tar -xvf linux-3.18.6.tar
cd linux-3.18.6
make i386_defconfig
make # 一般要编译很长时间,少则20分钟多则数小时
# 制作根文件系统
cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git # 如果被墙,可以使用附件menu.zip
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static –lpthread
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
# 启动MenuOS系统
cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
1.2 代码准备
下载孟宁老师的menu代码,并修改其中的test.c函数。修改后如下:
char* path = "/root/test_c";
int Mkdir(int argc, char *argv[])
{
int tt;
char* path = "/root/test_c";
unsigned short mod = 0750;
tt = mkdir(path, mod);
printf("mkdir ret = :%d\\n",tt);
return 0;
}
int MkdirAsm(int argc, char *argv[])
{
int tt;
char* path = "/root/test_asm";
unsigned short mod = 0750;
asm volatile(
"mov $0x27, %%eax\\n\\t"
"int $0x80\\n\\t"
"mov %%eax, %0\\n\\t"
:"=m"(tt)
:"b"(path),"c"(mod)
);
printf("mkdir asm ret = :%x\\n",tt);
return 0;
}
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("time","Show System Time",Time);
MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
MenuConfig("mkdir","create folder",Mkdir);//新加代码
MenuConfig("mkdir_asm","create folder(asm)",MkdirAsm); //新加代码
ExecuteMenu();
}
编译函数
[root@localhost menu]# gcc -o init linktable.c menu.c test.c -m32 -static -lpthread
/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../libpthread.a(libpthread.o): In function `sem_open':
(.text+0x708a): warning: the use of `mktemp' is dangerous, better use `mkstemp'
[root@localhost menu]#
重新制作根文件系统
1.3 gdb命令准备
按照课件
使用gdb跟踪调试内核
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
另开一个shell窗口
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
2. 实验过程
2.1 gdb断点设置
当系统收到系统调用的中断信号后,会调用entry_32.S中的
因此我们将断点设置在ENTRY处。
(gdb) b sys_mkdir
Breakpoint 1 at 0xc113db10: file fs/namei.c, line 3525.
(gdb) info line entry_32.S:493 ----找到entry_32.S的entry所在的内存地址
Line 493 of "arch/x86/kernel/entry_32.S" starts at address 0xc176b747 and ends at 0xc176b748.
(gdb) b *0xc176b747 ---- 在内存地址上面打上断点
Breakpoint 2 at 0xc176b747: file arch/x86/kernel/entry_32.S, line 493.
打好断点后,开始执行。因为我们是在system_call处打上断点,所有的中断都会从此入口,我们需要在跟踪的时候,需要判断一下是否是我们需要的。
2.2 实验开始
2.2.1 调用内核sys_mkdir之前
1) 在弹出窗口处输入 我们之前设置好的命令mkdir_asm
2). 此时我们可以看到gdb停在我们之前设置的断点处了,查看一下eax的值正是我们之前设置的0x27
Breakpoint 2, ?? () at arch/x86/kernel/entry_32.S:493
493 pushl_cfi %eax # save orig_eax
(gdb) info reg eax
eax 0x27 39
在gdb中执行ni命令, 发现系统进行SAVE_ALL,因此SAVE_ALL为宏定义,因此gdb无法正常显示其代码
(gdb) ni
0xc176b764 494 SAVE_ALL ----保存堆栈值
(gdb) ni
接下来的是判断是否需要开启子跟踪。
在《Linux内核情景分析》中有一段话,是如下描述的:“在task_struct数据结构中有个成分flags,其中的标志位叫PT_TRACESYS.一个进程可以通过系统调用ptrace(),将一个子进程的PT_TRACESYS标志位调成1,从而跟踪该子进程的系统调用。linux系统中有一条命令strace就是干这件事的,是一个很有用的工具。”
495 GET_THREAD_INFO(%ebp)
(gdb) ni
0xc176b76d 495 GET_THREAD_INFO(%ebp)
(gdb) ni
497 testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
(gdb) ni
498 jnz syscall_trace_entry
(gdb) ni
接下来就开始准备进入系统调用了。
499 cmpl $(NR_syscalls), %eax --- 对比一下传入的系统调用号是否合法
(gdb) ni
500 jae syscall_badsys ----- 不合法,则跳入到异常处理
(gdb) ni
502 call *sys_call_table(,%eax,4) ---合理,则进入我们的内核的mkdir处理函数
sys_call_table为一个函数数组,系统通过系统调用号39找到对应的函数。
2.2.2 调用sys_mkdir之后
当系统调用内核的代码sys_mkdir之后
它首先是保存eax的值。紧接着禁止中断,从保证接下来的处理不会被打断。
其中DISABLE_INTERRUPTS的定义如下:
define DISABLE_INTERRUPTS(x) cli
其调试信息如下:
(gdb) n
?? () at arch/x86/kernel/entry_32.S:504
504 movl %eax,PT_EAX(%esp) # store the return value
(gdb) info reg ecx
ecx 0xb92 2962
(gdb) n
507 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
(gdb) n
511 movl TI_flags(%ebp), %ecx
(gdb) info reg ecx
ecx 0xb92 2962
(gdb) n
512 testl $_TIF_ALLWORK_MASK, %ecx # current->work
(gdb) info reg ecx
ecx 0x0 0
(gdb) n
513 jne syscall_exit_work
(gdb) n
519 movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
(gdb) n
523 movb PT_OLDSS(%esp), %ah
在第512行会进行判断,是进行syscall_exit_work调转的关键。当ecx的值为零时:
而当ecx的值不为零时,它就会跳转
此处一直不是太明白,为什么同样的执行动作ecx的值会不一样?
在另一个网友的博客《Linux从用户层到内核层系列 - TCP/IP协议栈部分系列11: 再话Linux系统调用 》中是这样描述的:
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx //寄存器ecx是通用寄存器,在保护模式中,可以作为内存偏移指针(此时,DS作为 寄存器或段选择器),此时为返回到系统调用之前做准备
testl $_TIF_ALLWORK_MASK, %ecx //TEST 测试.(两操作数作与运算,仅修改标志位,不回送结果).
2.2.3 中断的进入
2.2.3.1 中断表的实始化
在分析中断调用之前,先看一下中断表的初始化过程。
中断表是通过start_kernel中的trap_init进行初始化的。针对系统调用,只分析system_call的过程
void __init trap_init(void)
{
... ...
#ifdef CONFIG_X86_32
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif
... ...
}
其中SYSCALL_VECTOR的值为
# define SYSCALL_VECTOR 0x80
而set_system_trap_gate函数的实现:
static inline void set_system_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
}
其中GATE_TRAP就对应着陷阱门, n的值就就是0x80,而addr的值就是system_call的入口函数地址。查看一下_set_gate的实现:
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
//对应的参数:gate -- 0x80
// type -- GATE_TRAP,为一个枚举值,对应着GATE_TRAP = 0xF,
// addr -- 对应着函数的入口地址,也就是system_call
//dpl --- 权限,也就是0x3
//ist -- 权限,也就是内核态0
//seg -- 门的段描述符,通过__KERNEL_CS我们可以查找到GDT表中的相应表项,从而得到段基址
//pack_gate的作用就是按照LINUX内核要求设置代码地址设置
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
/*
* does not need to be atomic because it is only done once at
* setup time
*/
//设置idt_table表
write_idt_entry(idt_table, gate, &s);
write_trace_idt_entry(gate, &s);
}
而write_idt_entry也很简单
static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{
memcpy(&idt[entry], gate, sizeof(*gate));
}
将idt[0x80]这个值设置成我们需要的system_call值。
2.2.3.2 中断进入
当通过一条INT指令进入一个中断服务程序时,在指令中给出一个中断向量。CPU先根据该向量与中断向量表中找到一扇门,在这种情况下一般总是中断门。然后,就要将这个门的DPL与CPU的CPL相比,CPL必须小于或等于DPL,也就是优先级不低于DPL,才能穿过这扇门。穿过了中断门之后,还要进一步将目标代码段描述项中的DPL与CPL比较,目标段的DPL必须小于或等于CPL。
实验结论
- INT 0X80是由CPU处理,然后通过查找linux初始化的IDT表,找到system_call的地址,也就是entry_32.S的地址
- 当进入到entry_32.S后,首先会保存栈信息。EAX保存系统调用号
- 系统再根据系统调用号查找对应的系统调用表sys_call_table,而这个表中存放着实际的内核 处理函数。
- 当内核处理完成后会返回到entry_32.S中,此时系统会进行一系列的处理,如关中断等,再返回到用户态。从而完成整个系统调用。
参考:
1. linux 3.5.4 系统调用分析 http://blog.csdn.net/shen332401890/article/details/17434425
2. gdb 跟踪调试命令整理: http://www.cnblogs.com/kzloser/archive/2012/09/21/2697185.html
3. Linux从用户层到内核层系列 - TCP/IP协议栈部分系列11: 再话Linux系统调用 : http://blog.csdn.net/byhankswang/article/details/9412093?utm_source=tuicool&utm_medium=referral
4. 《Linux内核源代码情景分析(上册)》 毛德操 / 胡希明著
以上是关于通过系统调用分析system_call中断处理过程的主要内容,如果未能解决你的问题,请参考以下文章