《第一篇 linux 0.12 系统调用(int 0x80)详解》

《第一篇 linux 0.12 系统调用(int0x80)详解》

 

 

  • 系统调用初始化 

在系统启动时,会在sched_init(void)函数中调用set_system_gate(0x80,&system_call),设置中断向量号0x80的中断描述符:

#define set_intr_gate(n,addr) 	_set_gate(&idt[n],14,0,addr)

#define set_trap_gate(n,addr) 	_set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr) _set_gate(&idt[n],15,3,addr)

#define _set_seg_desc(gate_addr,type,dpl,base,limit) ...... 

其中15指明是陷阱门描述符,注意,这个中断向量不是中断门描述符(14),此陷阱门描述符的DPL为3,addr为此陷阱门对应的中断处理过程的32位偏移地址。因为中断处理过程属于内核,所以段选择符为0x0008(内核代码段)。因为DPL为3,所以通过set_system_gate设置的中断处理过程能被所有程序执行。

中断门与陷阱门的区别在于对EFLAGS的中断允许标志IF的影响。通过中断门描述符执行中断会复位IF标志,以被免其它中断干扰当前中断的处理,并且其它的中断结束指令IRET会从堆栈上恢复IF标志的原值。而通过陷阱门执行中断则不会影响IF标志。 

  • 系统调用的执行--从用户态到内核态的过程

当执行int 0x80时,CPU会通过中断向量号0x80找到对应的中断描述符项,这中断描述符项是在初始化时设置的陷阱门(set_system_gate(0x80,&system_call))。此陷阱门含有一个长指针:段选择符(内核代码段)和偏移值(system_call函数地址)。由于中断是通过int n产生的,CPU才会检查中断或陷阱门中的DPL,此时CPL必须小于或等于门的DPL,这个限止可以防止运行在特权级3的应用程序使用软中断访问重要的异常处理程序,如缺页处理程序:set_trap_gate(14,&page_fault)。

当CPU执行完权限检查后,CPU会从当前任务的TSS段中得到中断处理程序使用的栈段选择符和栈指针(tss.ss0 & tss.esp0),然后将栈选择符和栈指针压入新栈中。如果特权级不发生变化(比如内核内部调用),直接将EFLAGS、CS、EIP压入当前被中断程序的栈中。如果特权变化(用户态过程执行系统调用),则将原SS、原ESP、EFLAGS、CS、EIP压入中断过程使用的新栈,即进程的内核态栈,此时ESP指向了新栈。

然后,CPU从中断描述符中取得CS:IP,此时CS为0x8(内核代码段),IP为system_call函数地址,CPU完成从用户太到内核态切换,开始执行system_call函数。

测试代码

//gcc -fno-builtin -m32 -c main.c; ld -static -e no_main -o run main.o -m elf_i386

char* str = "hello world!\n";
void print()
{
  asm("movl $13, %%edx  \n\t"
      "movl %0, %%ecx   \n\t"
      "movl $1, %%ebx   \n\t"
      "movl $4, %%eax   \n\t"
      "int $0x80  \n\t"
      ::"r"(str));
        
  asm("int $0x04");     
  asm("int $0x0");          
}

void exit()
{
  asm("movl $42, %ebx   \n\t"
      "movl $1, %eax  \n\t"
      "int $0x80 \n\t");
}

int no_main() {
  print();
  exit();
  return 0;
  
}

Program received signal SIGSEGV, Segmentation fault.
0x080480b1 in print ()
(gdb) disassemble 
Dump of assembler code for function print:
   0x08048094 <+0>:     push   %ebp
   0x08048095 <+1>:     mov    %esp,%ebp
   0x08048097 <+3>:     mov    0x8049160,%eax
   0x0804809c <+8>:     mov    $0xd,%edx
   0x080480a1 <+13>:    mov    %eax,%ecx
   0x080480a3 <+15>:    mov    $0x1,%ebx
   0x080480a8 <+20>:    mov    $0x4,%eax
   0x080480ad <+25>:    int    $0x80
   0x080480af <+27>:    int    $0x4
=> 0x080480b1 <+29>:    int    $0x0  //int $0x0的DPL是0,权限检查不过,core dump
   0x080480b3 <+31>:    pop    %ebp
   0x080480b4 <+32>:    ret    
End of assembler dump.


Int 0x80的输入输出参数说明:
输入参数:eax=功能号(比如2为fork系统调用)
返回值:EAX=sys_fork函数的返回值
功能号对应sys_call_table[]的下标,比如sys_call_table[2]表示fork系统调用函数。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,

 

  • system_call的实现

当调用_system_call函数时,CPU就从用户态进入了内核态。注意,特权变化了!对于x86系统,因为所有的寄存器都只有一个物理寄存器(ARM就不一要样了),因为内核态与用户态共享所有寄存器(段、通用、栈寄存器),比如SS、ESP、eflags、CS、EIP这五个寄存器。为了能够从内核态返回到调用处继续执行,当前现场,即相关寄存器的内容都需要被保存起来。

那么,这些现场信息能保存到用户态堆栈么,如果保存了用户态堆栈,当然不行。那么,这些栈内存区域,用户程序就可以必改变,那么,程序就很容易被攻击了,直接修改CS:EIP对应的栈内存,那么,你懂的^_^。

现场信息是保存在当前进程的内核态堆栈中,当执行int n指令时,CPU自动把SS、ESP、eflags、CS、EIP五个寄存器压入了内核栈,然后,在保存相关通用、段之类寄存器。调用中断处理程序,当从中断处理程序返回时,最后会执行iret指令从内核返回用户态时,此时这五个寄存器会自动从内核栈中恢复。

         _system_call部分代码分析:

         push %ds

         push %es

         push %fs

         pushl %eax                # save the orig_eax

         pushl %edx               

         pushl %ecx                 # push %ebx,%ecx,%edx asparameters

         pushl %ebx                # to the system call

         movl $0x10,%edx              // ds、es此时指向当前进程的内核态数据段

         mov %dx,%ds

         mov %dx,%es                    

         movl $0x17,%edx              //即使没这二行也行吧,fs本来就指向当前进程的用户态数据段

         mov %dx,%fs                      //因为在fork进程时,fs已经在copy_process函数中设置了。

         call_sys_call_table(,%eax,4)  //根据EAX传入的功能号,即可调用相关系统函数

         pushl %eax                                   //系统调用函数的返回值入栈

 


当在中断处理函数(陷阱门)中执行时,是可被中断(中断门)的,因为eflags标志中的TF被设置为允许中断的。因而有可能在时钟中断函数(do_timer)中,本进程的时间片可能被修改为0。所以,当从系统调用相关功能号对应函数返回时,需要检查当前进程是否还在就绪态,或时间片是否用完,并确认是否需要重新执行调度程序。

2:      movl _current,%eax

         cmpl $0,state(%eax)                 # state

         jne reschedule

         cmpl $0,counter(%eax)             # counter

         je reschedule

//系统调用返回时,会处理当前任务的信号,进程的信号识别与信号处理,仅在系统调用或时钟中断(每10ms)返回时。就能处理信号,优先级还是蛮高的,至少在进程执行流中,到少每10m就能处理信号。

ret_from_sys_call:

         movl _current,%eax

         cmpl _task,%eax                        # task[0] cannot havesignals

         je 3f

         cmpw $0x0f,CS(%esp)               # was old code segment supervisor?

         jne 3f

         cmpw $0x17,OLDSS(%esp)                # was stack segment = 0x17 ?

         jne 3f

        

         movl signal(%eax),%ebx

         movlblocked(%eax),%ecx

         notl %ecx

         andl %ebx,%ecx

         bsfl %ecx,%ecx

         je 3f

         btrl %ecx,%ebx

         movl %ebx,signal(%eax)

         incl %ecx

         pushl %ecx

         call _do_signal                            //调用信号处理函数处理当前进程信号

         popl %ecx

         testl %eax, %eax

         jne 2b                 # see if we need to switchtasks, or do more signals

3:      popl %eax

         popl %ebx

         popl %ecx

         popl %edx

         addl $4, %esp  # skip orig_eax

         pop %fs

         pop %es

         pop %ds

         iret            //此指令会将内核栈中的数据弹出到这5个寄存器SS、ESP、eflags、CS、EIP。


  •  执行系统调用相应功能号的函数: call_sys_call_table(,%eax,4) ,即可调用相关系统调用函数。如sys_execve。

 

 

你可能感兴趣的:(linux,0.12)