系统调用原理

以fork()为例

 

int main()

{

    fork();

}

fork()是一个对系统调用fork的封装,可用下列宏来定义

_syscall0(pit_t, fork);

_syscall0是一个宏函数,i386版本定义如下:

 

#define _syscall0(type, name)       \

type  name(void)                         \

{               \

  long __res;         \

  __asm__ volatile ( "int $0x80"         //__asm__ 为gcc 嵌入汇编代码标识关键字, volatile 告诉gcc 这段代码不做优化;__asm__的第一个参数为汇编代码字符串,0x80为中断号\

       : "=a" (__res)   // a表示eax, 这个语句表示用eax输出返回数据并存储在__res里。\

      : "0"  (__NR__##name)); //表示__NR_##name为输入,“0”指示有编译器选择和输出相同的寄存器(eax)来传递参数。\

   __syscall_return(type, __res); \

}

对于__syacall0(pid_t, fork)展开为:

pid_t fork(void)

{

  long __res;

  __asm__ volatile ("int $0x80"

  : "=a" (__res)

       : "0" (__NR_fork));

  __syscall_return(pid_t, __res);

}

进一步可读化:

pid_t fork(void)

{

  long __res;

  $eax = __NR_fork   //eax传递系统调用号, linux系统中fork为2。 linux/include/asm-x86/unistd_32.h   #define __NR_fork   2

  int $0x80

  __res = $eax

  __syacall_return(pid_t, __res);

}

 

带参数的syscall

#define _syscall2(type, name, type1, arg1)  \

type name(type1, arg1)                   \

{                   \

  long  _res;  \

  __asm__ volatile ("int $0x80" \

  : "=a" (__res)        \

  : "0" (__NR_##name), "b" ((long)(arg1))); // b代表EBX寄存器, "b"((long)(arg1))表示先把arg1强转为long,然后存放在EBX里作为输入。可用EBX/ECX/EDX/ESI/EDI/EBP \

       __syscall_return(type, __res); \

}

对应汇编代码:

push ebx

eax = __NR_##name

ebx = arg1

int 0x80

__res = eax

pop ebx

 

系统调用需要将堆栈从用户栈切换到内核栈,返回时需要切换到用户栈。

“当前栈”是指ESP的值所在的栈空间,ESP的值位于用户栈范围,则程序的当前栈就是用户栈,反之亦然。此外,寄存器SS的值还应该指向当前栈所在的页。所以用户栈--》内核栈 实际就是:

1)保存当前的ESP、SS值

2)将ESP、SS的值设为内核栈的相应值。

反过来, 内核栈--》用户栈则是:

1)恢复原来的ESP、SS的值

用户态的ESP、SS保存在哪里呢? 在内核栈上。有i386的中断硬件指令完成。

当0x80中断发生的时候,CPU除了切换内核态之外,还自动完成:

1)找到当前进程的内核栈(每个进程都有自己的内核栈)

2) 在内核栈中依次压入用户态的寄存器SS、ESP、EFLAGS、CS、EIP。

当内核从系统调用中返回的时候,需要用iret指令来回到用户态。

 

Linux实际不用int $0x80方式,而是用sysenter方式。

cat /proc/self/maps可以看到vdso文件(virtual dynamic shared library),vdso总是被加载在地址0xffffe000

....

ffffe000-fffff000 r-xp 00000000 00:00 0  [vdso]

通过

$dd if=/proc/self/mem of=linux-gate.dso bs=4096 skip=1048574 count=1

将vdso输出到linux-gate.dso. dd复制文件,if--输入文件,of--输出文件, skip表示从文件开头要跳过都少块, 0xffffe000/4096(0x1000) = 1048574

$objdump -T linux-gate.dso可分析该文件

.....

ffffe400 .... __kernel_vsyscall

查看该vsyscall,

objdump -d --start-address=0xffffe400  --stop-address=0xffffe408  linux-gate.dso

...

push %ecx

push %edx

push %ebp

mov  %esp, %ebp

sysenter

nop

 

人工调用系统调用

int main()

{

  int ret;

  char msg[] = "Hello\n";

  __asm__ volatile (

    "call *%%esi"

    : "=a" (ret)

    : "a" (4),    //write 系统调用号4通过eax传入

    “s”(0xffffe400),  //通过esi传入__kernel_vsyscall函数的地址

    ”b” ((long)1),  //fd = stdout 

     “c” ((long)msg),

               "d" ((long)sizeof(msg)));

  return 0;

}

}

 

 

posted @ 2021-11-20 23:42  平安1111  阅读(158)  评论(0)    收藏  举报