最近在研究X86汇编,经过研究发现在Linux中使用汇编调用诸如read/write/open/close etc的系统调用都是如下格式:
# this is for testing write
.section .data
output:
.asciz "the test string\n"
output_len:
.int .-output
.section .text
.globl main
main:
movl $4, %eax
movl $1, %ebx
movl $output, %ecx
movl output_len, %edx
int $0x80
pushl $0
call exit
以上格式说明:
1. %eax中传递的是系统调用号。
2. %ebx、%ecx、%edx…依次为传递的是系统调用的参数。
3. 使用系统调用时候开启0x80中断。
根据上面的格式说明抛出如下疑问:
1. 各个寄存器在系统调用的作用。
2. 0x80中断在linux系统调用时的作用是什么?
3. 系统调用的原理是什么?
1.各个寄存器在系统调用的作用
在X86系统中有 %eax, %ebx, %ecx, %edx, %esi, %edi, %esp, %eip, %eflags, %crx寄存器等,所谓寄存器就是硬件中CPU可以直接运行并且可以操作的数字,寄存器在运算中的作用就是通过寄存器可以操作内存中的地址, 它的重要性在内存之上。
系统运行时候只有各个寄存器协调作用CPU才算一个完整的CPU,才能够发挥作用。
1.1 %eax寄存器的作用
eax寄存器的作用就是传递给内核正确的系统调用号,并且内核识别到正确的系统调用号之后才能够正确的调用诸如read、write、open、close之类的系统调用
1.2 其余寄存器的作用
除了eax寄存器之外的其余寄存器我们叫做其它寄存器,比如:ebx、ecx、edx等等等
他们的作用是给在1.1
节中的传入eax寄存器中的系统调用号传递参数。
比如在文章开始的时候写的一个例子:
ebx传递数字1,代表传递给write的第一个参数为1,标准输出
ecx传递$output地址,write的第二个参数,为一个指针,在本处要显示输出的地址。
edx要写入的字符长度。
2. 0x80中断在Linux中系统调用的作用
int 0x80中断在CPU硬件层次手动触发一次中断,本中断为软中断。中断原理如下:在中断触发的时候,根据中断向量表中的地址判断中断要执行什么代码。
所谓的中断向量表就是我们的内核在Linux系统启动的时候分配的一段内存,内存的每四个字节代表触发一个中断系统要执行什么操作,例如触发0x80中断,CPU就会自动执行这段地址+0x80*4处的一个函数调用(system_call)。
也就是每触发一次0x80中断执行最先执行的就是system_call内核调用,system_call函数原型为一段汇编如下:
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
pushl %eax # save orig_eax
CFI_ADJUST_CFA_OFFSET 4
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
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
以上汇编函数的作用:
- 保存中断发生时候各个寄存器的值。
- 调用相关的系统调用,系统调用的所有函数存放在sys_call_table全局数组中。这个数组存放的是sys_write,sys_read, sys_open, sys_close等的系统调用的地址。
ps:搜索这些地址只有原型没有定义,因为这些系统调用大部分是由宏SYSCALL_DEFINE定义,故而没有定义,例如搜索sys_write时候应该查找SYSCALL_DEFINE中的定义,这些才是函数原型。
3. 系统调用的基本原理
通过以上两节的系统调用的分析我们可以把系统调用的阶段分为如下三个阶段:
1. 系统启动阶段。
2. 系统调用system_call阶段
3. system_call返回阶段
其中system_call返回阶段暂且不赘述。
3.1系统启动阶段
在系统启动阶段Linux内核在内核中分配了系统调用的函数地址,并且把这个函数地址放入:sys_call_table中的全局数组中,以便系统触发0x80中断时候找到各个系统调用的地址。
3.2 系统调用system_call阶段
系统调用system_call 阶段就是保存中断发生时的寄存器状态和寄存器值,并且调用sys_call_table中保存的相应的(%eax*4的偏移)函数。