通过系统调用分析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中的
通过系统调用分析system_call中断处理过程_第1张图片
因此我们将断点设置在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的值为零时:
通过系统调用分析system_call中断处理过程_第2张图片
而当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。
通过系统调用分析system_call中断处理过程_第3张图片

实验结论

  1. INT 0X80是由CPU处理,然后通过查找linux初始化的IDT表,找到system_call的地址,也就是entry_32.S的地址
  2. 当进入到entry_32.S后,首先会保存栈信息。EAX保存系统调用号
  3. 系统再根据系统调用号查找对应的系统调用表sys_call_table,而这个表中存放着实际的内核 处理函数。
  4. 当内核处理完成后会返回到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中断处理过程)