系统调用机制(sethostname)
用户空间
sethostname库函数
sethostname是glibc封装的库函数:
int sethostname(const char *name, size_t len);
查看sethostname的代码:
objdump -d /usr/lib/libc.a mov %ebx, %edx mov 0x8(%esp,1), %ecx mov 0x4(%esp,1), %ebx mov $0x4a, %eax int $0x80 mov %edx, %ebx cmp $0xfffff001, %eax jae 1a<sethostname+0x1a> ret
1) 系统调用参数是通过寄存器传递的,因为系用调用trap到内核中会发生栈切换,不能通过栈来传递参数。 系统调用的参数个数最多6个。 2) mov 0x8(%esp,1), %ecx 参数len到%ecx 3) mov 0x4(%esp,1), %ebx 参数name到%ebx 4) mov $0x4a, %eax sethostname的系统调用号 5) int $0x80 系统调用中断 6) cmp $0xfffff001, %eax jae 1a<sethostname+0x1a> 检查返回值如果在-4095到-1之间,说明有错误发生。 其中,sethostname+0x1a是__syscall_error,在链接过程中由连接器填入。
sethostname库函数返回出错的处理
__syscall_error的汇编代码:
neg %eax push %eax call 4<__syscall_error_1+0x2> pop %ecx mov %ecx, (%eax) mov $0xffffffff, %eax ret
1) neg %eax 把返回码取反,并且压入栈中。 2) call 4<__syscall_error_1+0x2> 把全局变量errono地址加载到%eax中。 3) mov %ecx, (%eax) 把错误码写入errono 4) mov $0xffffffff, %eax 将%eax的内容填入-1,系统调用出错的通用的返回码是-1.errono中有具体的错误码。
内核空间
系统调用对应的陷阱门
系统调用通过INT指令穿过陷阱门后到达了内核空间,并且发生了栈的切换。 陷阱门和中断门的差别就是:陷阱门的n全下级别是DPL为3. 穿过陷阱门进入内核状态并不关闭中断,所以系统调用是可以被中断的。
系统调用入口0x80的服务程序 system_call
ENTRY(system_call) pushl %eax # save orig_eax SAVE_ALL GET_CURRENT(%ebx) cmpl $(NR_syscalls),%eax jae badsys testb $0x02,tsk_ptrace(%ebx) # PT_TRACESYS jne tracesys call *SYMBOL_NAME(sys_call_table)(,%eax,4) movl %eax,EAX(%esp) # save the return value ENTRY(ret_from_sys_call)
1) pushl %eax 保存系统调用号 2) GET_CURRENT(%ebx) 当前进程的task_struct指针到%ebx 3) testb $0x02,tsk_ptrace(%ebx) jne tracesys 检查进程是否启用了PT_TRACESYS,跟踪子进程的系统调用(strace工具)。 4) call *SYMBOL_NAME(sys_call_table)(,%eax,4) SYMBOL_NAME(sys_call_table)(,%eax,4) => sys_call_table + %eax*4 sys_call_table[]是一个函数指针的数组
内核代码 sys_sethostname
asmlinkage long sys_sethostname(char *name, int len) { int errno; if (!capable(CAP_SYS_ADMIN)) return -EPERM; if (len < 0 || len > __NEW_UTS_LEN) return -EINVAL; down_write(&uts_sem); errno = -EFAULT; if (!copy_from_user(system_utsname.nodename, name, len)) { system_utsname.nodename[len] = 0; errno = 0; } up_write(&uts_sem); return errno; }
1) if (!capable(CAP_SYS_ADMIN)) 检查权限 2) down_write(&uts_sem); 获取锁 3) if (!copy_from_user(system_utsname.nodename, name, len)) 从用户空间拷贝数据
copy_from_user
copy_from_user汇编代码
copy_from_user 最终调用到宏__copy_user_zeroing中。
#define copy_from_user(to,from,n) \ (__builtin_constant_p(n) ? \ __constant_copy_from_user((to),(from),(n)) : \ __generic_copy_from_user((to),(from),(n)))
static inline unsigned long __generic_copy_from_user_nocheck(void *to, const void *from, unsigned long n) { __copy_user_zeroing(to,from,n); return n; }
#define __copy_user_zeroing(to,from,size) \ do { \ int __d0, __d1; \ __asm__ __volatile__( \ "0: rep; movsl\n" \ " movl %3,%0\n" \ "1: rep; movsb\n" \ "2:\n" \ ".section .fixup,\"ax\"\n" \ "3: lea 0(%3,%0,4),%0\n" \ "4: pushl %0\n" \ " pushl %%eax\n" \ " xorl %%eax,%%eax\n" \ " rep; stosb\n" \ " popl %%eax\n" \ " popl %0\n" \ " jmp 2b\n" \ ".previous\n" \ ".section __ex_table,\"a\"\n" \ " .align 4\n" \ " .long 0b,3b\n" \ " .long 1b,4b\n" \ ".previous" \ : "=&c"(size), "=&D" (__d0), "=&S" (__d1) \ : "r"(size & 3), "0"(size / 4), "1"(to), "2"(from) \ : "memory"); \ } while (0)
1) 参数输入部: : "=&c"(size), "=&D" (__d0), "=&S" (__d1) %0 -> size 和%ecx绑定 %1 -> __d0 和%edi绑定 %2 -> __d1 和%esi绑定 2) 参数输出部 : "r"(size & 3), "0"(size / 4), "1"(to), "2"(from) %3 -> size&3 和一个寄存器绑定 %4 -> size/4 和%0也就是%ecx绑定 %5 -> to 和%1也就是%edx绑定 %6 -> from 和%2也就是%edi绑定 3) "0: rep; movsl\n" rep指令执行串操作,从%esi 到 %edi循环执行%ecx次数。 拷贝4字节。 4) " movl %3,%0\n" "rep; movsb\n" 拷贝剩下的一个字节。 5) fixup和__ex_table 错误处理: 如果用户空间传递的from指针压根就是非法地址(没有建立缺页映射)怎么办? 当然可以在执行拷贝之前调用verify_area,通过task_struct中的mm检查from的合法性。 但是这样一方面做性能低,另一方面绝大时候的系统调用的参数都是合法的,做了无用功。 在最新的内核中是通过“事后补救”的方式:如果参数非法了,执行不下去了,恢复现场。而不是对所有的系统调用全部都进行参数检查。
fixup和__ex_table
对于可能出错的地方并不事先做sanity test,而采取“事后补救”。C++的异常借鉴了这种设计。 gcc编译器把代码编译后生成了text和data段,还支持fixup段和__ex_table段。 fixup:用于异常发生后的修复。 __ex_table:用户异常指令地址表。 .section .fixup 和 .section __ex_table 告诉编译器把相应的代码放在fixup和__ex_table段中。
".section __ex_table,\"a\"\n" " .align 4\n" " .long 0b,3b\n" " .long 1b,4b\n" struct exception_table_entry { unsigned long insn, fixup; };
指示标号0处指令的异常由标号3处的代码处理,标号1处指令的异常由4号处理。 exception_table_entry结构体就存储了异常指令和对应的修复地址。 同时,gcc把所有的 exception_table_entry结构体按照异常的指令地址排序。
fixup和__ex_table的使用
"0: rep; movsl\n" 标号0处的指令发生异常的情况是esi,edi对应的内存地址是非法地址。
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code) { if (in_interrupt() || !mm) goto no_context; vma = find_vma(mm, address); if (!vma) goto bad_area; no_context: if ((fixup = search_exception_table(regs->eip)) != 0) { regs->eip = fixup; return; } }
1) (fixup = search_exception_table(regs->eip)) != 0) 在__ex_table段中二份查找出错的指令对应的修复地址 2) regs->eip = fixup; 把找到的修复地址添入eip中。下一条要执行的指令就是fixup了。 下面两个函数是在__ex_table表中查找修复地址。
unsigned long search_exception_table(unsigned long addr) { unsigned long ret; ret = search_one_table(__start___ex_table, __stop___ex_table-1, addr); if (ret) return ret; return 0; }
static inline unsigned long search_one_table(const struct exception_table_entry *first, const struct exception_table_entry *last, unsigned long value) { while (first <= last) { const struct exception_table_entry *mid; long diff; mid = (last - first) / 2 + first; diff = mid->insn - value; if (diff == 0) return mid->fixup; else if (diff < 0) first = mid+1; else last = mid-1; } return 0; }
系统调用的返回 ret_from_sys_call
系统调用的返回和中断处理的返回逻辑一致都要检查是否有软中断要处理
ENTRY(ret_from_sys_call) movl SYMBOL_NAME(irq_stat),%ecx # softirq_active testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask jne handle_softirq ret_with_reschedule: cmpl $0,need_resched(%ebx) jne reschedule cmpl $0,sigpending(%ebx) jne signal_return restore_all: RESTORE_ALL
1) movl SYMBOL_NAME(irq_stat),%ecx 加载irq_stat中的active 2) testl SYMBOL_NAME(irq_stat)+4,%ecx 测试 softirq_active & softirq_mask 3) jne handle_softirq 如果非零在从系统调用返回前处理软中断。 4) RESTORE_ALL 通过iret返回到用户空间
RESTORE_ALL
#define SAVE_ALL \ cld; \ pushl %es; \ pushl %ds; \ pushl %eax; \ pushl %ebp; \ pushl %edi; \ pushl %esi; \ pushl %edx; \ pushl %ecx; \ pushl %ebx; \ movl $(__KERNEL_DS),%edx; \ movl %edx,%ds; \ movl %edx,%es;
#define RESTORE_ALL \ popl %ebx; \ popl %ecx; \ popl %edx; \ popl %esi; \ popl %edi; \ popl %ebp; \ popl %eax; \ 1: popl %ds; \ 2: popl %es; \ addl $4,%esp; \ 3: iret; \ .section .fixup,"ax"; \ 4: movl $0,(%esp); \ jmp 1b; \ 5: movl $0,(%esp); \ jmp 2b; \ 6: pushl %ss; \ popl %ds; \ pushl %ss; \ popl %es; \ pushl $11; \ call do_exit; \ .previous; \ .section __ex_table,"a";\ .align 4; \ .long 1b,4b; \ .long 2b,5b; \ .long 3b,6b; \ .previous
系统调用号
系统调用编号
在include/asm-i386/unistd.h文件中定义了系统调用的编号
#define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4
系统调用表sys_call_table的初始化
ENTRY(sys_call_table) .long SYMBOL_NAME(sys_ni_syscall) .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) .long SYMBOL_NAME(sys_read) .long SYMBOL_NAME(sys_write) .long SYMBOL_NAME(sys_open) /* 5 */ .long SYMBOL_NAME(sys_close) .long SYMBOL_NAME(sys_waitpid) .long SYMBOL_NAME(sys_creat)