薛兆江 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
使用gdb跟踪分析一个系统调用内核函数,分析系统调用的过程,从system_call开始到iret结束之间的整个过程,并画出简要准确的流程图
1.main.c中start_kernel函数:trap_init()函数中有一句 set_system_trap_gate(SYSCALL_VECTOR,&system_call)。
其中:SYSCALL_VECTOR:系统调用的中断向量(#define SYSCALL_VECTOR 0x80),&system_call:汇编代码入口
2.一执行int $0x80,系统直接跳转到system_call。
3.SAVE_ALL:保存现场
4.call *sys_call_table(,%eax,4)调用了系统调度处理函数,eax存的是系统调用号,是实际的系统调度程序。 sys_call_table:系统调用分派表
5.syscall_after_all:保存返回值
6.syscall退出,若有sys_exit_work,则进入sys_exit_work:执行的目的是处理进程之间的信号或者进程调度。
7.若无sys_exit_work,就执行restore_all恢复现场。
8.返回用户态,INTERRUPT_RETURN <=> iret,结束。
在LinuxKernel/menu/test.c中添加如图所示的代码
运行自动编译脚本,生成根文件系统
void __init trap_init(void)
{
int i;
#ifdef CONFIG_EISA
void __iomem *p = early_ioremap(0x0FFFD9, 4);
if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))
EISA_bus = 1;
early_iounmap(p, 4);
#endif
set_intr_gate(X86_TRAP_DE, divide_error);
set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
/* int4 can be called from all */
set_system_intr_gate(X86_TRAP_OF, &overflow);
set_intr_gate(X86_TRAP_BR, bounds);
set_intr_gate(X86_TRAP_UD, invalid_op);
set_intr_gate(X86_TRAP_NM, device_not_available);
#ifdef CONFIG_X86_32
set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);
#endif
set_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun);
set_intr_gate(X86_TRAP_TS, invalid_TSS);
set_intr_gate(X86_TRAP_NP, segment_not_present);
set_intr_gate(X86_TRAP_SS, stack_segment);
set_intr_gate(X86_TRAP_GP, general_protection);
set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug);
set_intr_gate(X86_TRAP_MF, coprocessor_error);
set_intr_gate(X86_TRAP_AC, alignment_check);
#ifdef CONFIG_X86_MCE
set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK);
#endif
set_intr_gate(X86_TRAP_XF, simd_coprocessor_error);
/* Reserve all the builtin and the syscall vector: */
for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
set_bit(i, used_vectors);
#ifdef CONFIG_IA32_EMULATION
set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif
#ifdef CONFIG_X86_32
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif
/*
* Set the IDT descriptor to a fixed read-only location, so that the
* "sidt" instruction will not leak the location of the kernel, and
* to defend the IDT against arbitrary memory write vulnerabilities.
* It will be reloaded in cpu_init() */
__set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO);
idt_descr.address = fix_to_virt(FIX_RO_IDT);
/*
* Should be a barrier for any external CPU state:
*/
cpu_init();
x86_init.irqs.trap_init();
#ifdef CONFIG_X86_64
memcpy(&debug_idt_table, &idt_table, IDT_ENTRIES * 16);
set_nmi_gate(X86_TRAP_DB, &debug);
set_nmi_gate(X86_TRAP_BP, &int3);
#endif
}
第839行set_system_trap_gate(SYSCALL_VECTOR, &system_call);设置了系统调用的中断向量(即系统调用的入口地址),即system_call“函数”的地址绑定到中断向量。所以在int $0x80之后,就转到中断向量,从而执行中断处理程序(系统调用程序)。也就是说中断向量指向的中断处理程序就是int $0x80指令执行完之后接着执行的指令的地方。
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
ASM_CLAC
pushl_cfi %eax # save orig_eax
SAVE_ALL//保存现场
GET_THREAD_INFO(%ebp)//获取thread_info结构的信息
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)// 测试是否有系统跟踪
jnz syscall_trace_entry//如果有系统跟踪,先执行,然后再回来
cmpl $(NR_syscalls), %eax//比较eax中的系统调用号和最大syscall,超过则无效
jae syscall_badsys//无效的系统调用 直接返回
syscall_call:
call *sys_call_table(,%eax,4)//系统调用处理程序使用eax传过来的系统调用号查表来执行相应函数
syscall_after_call://保存返回值
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work//检测是否所有工作已完成
jne syscall_exit_work//未完成,则去执行这些任务
restore_all:
TRACE_IRQS_IRET
restore_all_notrace:
#ifdef CONFIG_X86_ESPFIX32
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss # returning to user-space with LDT SS
#endif
restore_nocheck://恢复现场
RESTORE_REGS 4 # skip orig_eax/error_code
irq_return:
INTERRUPT_RETURN
.section .fixup,"ax"
.macro SAVE_ALL
cld
PUSH_GS
pushl_cfi %fs
/*CFI_REL_OFFSET fs, 0;*/
pushl_cfi %es
/*CFI_REL_OFFSET es, 0;*/
pushl_cfi %ds
/*CFI_REL_OFFSET ds, 0;*/
pushl_cfi %eax
CFI_REL_OFFSET eax, 0
pushl_cfi %ebp
CFI_REL_OFFSET ebp, 0
pushl_cfi %edi
CFI_REL_OFFSET edi, 0
pushl_cfi %esi
CFI_REL_OFFSET esi, 0
pushl_cfi %edx
CFI_REL_OFFSET edx, 0
pushl_cfi %ecx
CFI_REL_OFFSET ecx, 0
pushl_cfi %ebx
CFI_REL_OFFSET ebx, 0
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
movl $(__KERNEL_PERCPU), %edx
movl %edx, %fs
SET_KERNEL_GS %edx
.endm
指令中的一些小疑惑的地方:.macro和.endm,用.MACRO指令你可以定义一个宏,可以把需要重复执行的一段代码,或者是一组指令缩写成一个宏
在这段代码中,保存了相关寄存器的值。它们是:ES,DS,EAX,EBP,EDI,ESI,EDX,ECX,EBX等等。从这里寄存器的顺序可以知道压栈的最后压入的是ebx,这里压入的栈是内核栈。
.macro RESTORE_INT_REGS
popl_cfi %ebx
CFI_RESTORE ebx
popl_cfi %ecx
CFI_RESTORE ecx
popl_cfi %edx
CFI_RESTORE edx
popl_cfi %esi
CFI_RESTORE esi
popl_cfi %edi
CFI_RESTORE edi
popl_cfi %ebp
CFI_RESTORE ebp
popl_cfi %eax
CFI_RESTORE eax
.endm
.macro RESTORE_REGS pop=0
RESTORE_INT_REGS
1: popl_cfi %ds
/*CFI_RESTORE ds;*/
2: popl_cfi %es
/*CFI_RESTORE es;*/
3: popl_cfi %fs
/*CFI_RESTORE fs;*/
POP_GS \pop
.pushsection .fixup, "ax"
4: movl $0, (%esp)
jmp 1b
5: movl $0, (%esp)
jmp 2b
6: movl $0, (%esp)
jmp 3b
.popsection
_ASM_EXTABLE(1b,4b)
_ASM_EXTABLE(2b,5b)
_ASM_EXTABLE(3b,6b)
POP_GS_EX
.endm
从上面代码中可以看出SAVE_ALL和RESTORE_INT_REGS是对应的
源码几百行过于复杂,孟老师将它简化几十行的汇编伪代码:
.macro INTERRUPT_RETURN
iret
.endm
.marco SAVE_ALL
...
.endm
.macro RESTORE_INT_REGS
...
.endm
ENTRY(system_call)
SAVE_ALL
syscall_call:
call *sys_call_table(%eax,4)
movl %eax,PT_EAX(%esp) #store the return value
syscall_exit:
testl $_TLF_ALLWORK_MASK,%ecx #current->work//检查是否还有其它工作要完成
jne syscall_exit_work
restore_all://恢复处理器状态
RESTORE_INT_REGS
irq_return:
INTERRUPT_RETURN
ENDPROC(system_call)
syscall_exit_work:
testl $_TIF_WORK_SYSCALL_EXIT,%ecx//测试syscall退出信号
jz work_pending //还有其它工作要做
END(syscall_exit_work)
work_pending:
testb $_TIF_NEED_RESCHED,%cl //检查是否需要重新调度
jz work_notifysig //不需要重新调度
work_resched://需要重新调度
call schedule //调度进程
jz retore_all//没有其它的事,则恢复处理器状态
work_notifysig: #deal with pending signals//处理挂起的信号
...
END(work_pending)
int 0x80之后发生了什么呢?
第一步硬件细节:把cs:eip的值存储内核栈中,把ss:esp的值存储内核栈中,把eflags的值存储内核栈中
第二步硬件细节:更新cs:eip的值为0x80的中断服务函数
第三步硬件细节:将ss:esp的值指向了内核栈中的栈顶位置。
第四步软件细节:SAVE_ALL 在SAVE_ALL中我们看到了都是pushl的操作,其实是将这些值都存储进入了内核栈中。并且存储的部分寄存器的值,将会起到输入参数的功能。
当中断服务函数完成后
第五步软件细节:RESTORE_ALL 与SAVE_ALL的值是对应的。
第六步硬件细节:iret,这个iret与我们函数的ret是不同的,是与上述第一步的硬件的细节相互对应的。 将会从内核栈中将:eflags、ss:esp、cs:eip这三个值依次恢复到寄存器中,并换回到用户态中来。