Linux系统调用:使用int 0x80

6 篇文章 0 订阅

系统调用

系统调用的概念

系统调用是计算机程序请求操作系统内核服务的方式,包括硬件相关的服务(例如访问硬盘驱动器)、创建和执行新的进程和进程调度等等。系统调用提供了进程和操作系统间的必要接口。

在大多数操作系统中,系统调用只能被用户空间进程使用。而在某些操作系统中,比如在OS/360及其后续的一些操作系统中,有特权的系统代码也会触发系统调用。

系统调用的分类

系统调用大体上可分为5类:

  • 进程控制
    • 加载
    • 执行
    • 结束,中止
    • 创建进程
    • 结束进程
    • 得到/设置进程属性
    • 等待(时间、时间、信号)
    • 内存的分配和去配
  • 文件管理
    • 文件的创建和删除
    • 打开和关闭
    • 读、写和重定位
    • 得到/设置文件属性
  • 设备管理
    • 设备的请求和释放
    • 读、写和重定位
    • 得到/设置设备属性
    • 设备的逻辑关联或去关联
  • 信息维护
    • 得到/设置时间或日期
    • 得到/设置系统数据
    • 得到/设置进程、文件或设备属性
  • 通信
    • 通信连接的创建和删除
    • 发送、接收信息
    • 转换状态信息
    • 远程设备的关联或去关联

Linux系统调用:使用 int 0x80

Linux提供了200多个系统调用,通过汇编指令 int 0x80 实现,用系统调用号来区分入口函数。

Linux实现系统调用的基本过程是:

  • 应用程序准备参数,发出调用请求;
  • C库封装函数引导。该函数在Linux提供的标准C库,即 glibc 中。对应的封装函数由下列汇编指令实现(以读函数调用为例):
; NASM
; read(int fd, void *buffer, size_t nbytes)
mov eax, 3          ; read系统调用号为3
mov ebx, fd
mov ecx, buffer
mov edx, nbytes
int 0x80            ; 触发系统调用
  • 执行系统调用。前两步在用户态工作,陷入后在内核态工作。系统调用处理程序根据系统调用号,按系统调用表中的偏移地址跳转,调用对应的内核函数;
  • 系统调用完成相应功能,将返回值存入 eax ,返回到中断处理函数;
  • 系统调用返回。内核函数处理完毕后,库函数读寄存器( eax )返回值,并返回给应用程序。恢复现场。

应用程序调用系统调用的过程是:

  • 把系统调用号存入 eax
  • 把函数参数存入其它通用寄存器(约定顺序为 ebxecxedxesiedi ,更多的参数(通常不会出现这种情况)使用堆栈传递,也可以通过寄存器存放指向参数在用户空间的地址指针来传递);
  • 触发 0x80 号中断( int 0x80 )。
  • 示例:
; NASM
; 向显示器输出hello, world
; write(int fd, const void *buffer, size_t nbytes)
; exit(int status)
        global  _start
        section .text
_start:
        mov     eax, 4              ; write系统调用号为4
        mov     ebx, 1              ; 文件描述符1:标准输出stdout
        mov     ecx, message        ; 要输出的信息
        mov     edx, message.len    ; 要输出的长度
        int     0x80

        mov     eax, 1              ; exit系统调用号为1
        mov     ebx, 0              ; 状态码0:正常退出
        int     0x80

        section .data
message:
        db      "hello, world", 10
.len    equ     $ - message

Linux系统调用实现机制

系统调用初始化

系统调用处理程序 system_call() 的入口地址放在系统的中断表述符表IDT(Interrupt Descriptor Table)中,Linux系统初始化时,由 trap_init() 将其填写完整,其设置系统调用处理程序的语句为:

set_system_gate(0x80, &system_call)

经过初始化以后,每当执行 int 0x80 指令时,产生一个异常使系统陷入内核空间并执行128号异常处理程序,即系统调用处理程序 system_call()

系统调用公共入口

system_call() 是所有系统调用的公共入口,其功能是保护现场,进行正确性检查,根据系统调用号跳转到具体的内核函数。内核函数执行完毕时需调用 ret_from_sys_call() ,这时完成返回用户空间前的最后检查,用 RESTORE_ALL 宏恢复现场并执行 iret 指令返回用户断点。

保护现场

  • 硬件(CPU)保护:ssespeflagscseip ,压入核心栈;
  • 软件(操作系统)保护
    • 使用 SAVE_ALL 宏将寄存器压入堆栈,加载内核的 dses ,往 edx 中放入 $(_KERNEL_DS) 以指明使用内核数据段,把内核数据段选择符装入 dses 。注意:该宏压入寄存器的顺序不是随意的,而是和系统调用的参数传递密切相关;
    • esdseaxebpediesiedxecxebx ,压入核心栈。

系统调用处理时的核心栈内容:

硬件完成
ss
esp
eflags
cs
eip
软件完成
es
ds
eax
ebp
edi
esi
edx
ecx
ebx

返回值传递

当内核函数返回到 system_call() 时, eax 中存放着内核函数的返回值。要将这个返回值传递给应用程序,内核先将 eax 放入原先 SAVE_ALL 宏保存 eax 的位置,这样当 system_call() 调用 RESTORE_ALL 恢复寄存器时, eax 便被恢复成系统调用的返回值,完成了返回值从内核空间到用户空间的传递。

系统调用号和系统调用表

系统调用的数量由 NR_syscalls 宏给定,每个系统调用所对应的编号已预先在系统文件中定义,且都用一个宏表示,其定义有如下形式:

#define _NR_exit 1
#define _NR_fork 2
#define _NR_read 3
...

Linux的系统调用号和内核函数映射关系的系统调用表也被预先定义在系统文件中,具有如下形式:

.data
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)
...

内核函数入口地址为: eax * 4 + sys_call_table

参考

[1] 维基百科(英文) - 系统调用:Wikipedia - System call
[2] 维基百科(中文) - 系统调用:维基百科 - 系统调用
[3] 《操作系统教程(第五版)》(费翔林、骆斌编著,高等教育出版社):1.3.4 Linux系统调用及其实现机制

更多资料

[1] Linux系统调用表:Linux系统调用表
[2] NASM入门教程:NASM Tutorial
[3] NASM官方文档:NASM官方文档

  • 19
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值