---- 整理自 王利涛老师 课程
实验环境:宅学部落 www.zhaixue.cc
文章目录
- 1. 系统调用基本概念
- 1.1 一个系统调用的例子
- 1.2 什么是系统调用?软件复用的角度
- 2. 软中断:系统调用的入口
- 2.1 权限管理
- 2.2 系统调用号
- 2.4 man 2 syscall
- 2.5 实验:arm 系统调用
- 2.6 实验:x86 系统调用
- 3. 系统调用接口的封装
- 3.1 glibc
- 3.1.1 C 标准库
- 3.1.2 x86 平台
- 3.1.3 arm 平台
- 3.2 syscall
- 3.2.1 x86_64 下
- 3.2.2 arm 下
- 4. 系统调用流程分析
- 5. 添加一个系统调用
- 6. 系统调用的开销
- 6.1 主要开销
- 6.2 解决之道
- 6.2.1 快速系统调用
- 6.2.2 虚拟系统调用:vsyscall
- 6.2.3 虚拟动态共享对象:VDSO
- 6.2.3.1 VDSO:virtual dynamic shared object
- 6.2.3.2 VVAR:VDSO data page
- 6.2.3.3 实验:反汇编 VDSO 动态库
- 7. 文件的读写流程
1. 系统调用基本概念
1.1 一个系统调用的例子
1.2 什么是系统调用?软件复用的角度
main.c – add() – add.c add.h add.o ⇒ ar ⇒ libmath.so / libmath.a
main.c – sub() – sub.c sub.h sub.o ⇒ ar ⇒ libmath.so / libmath.a
- glibc – 默认链接
- OS:硬件资源的封装,任务创建、调度,读写磁盘等
如 ucos:OSInit() – OSTaskCreate() – 创建一个任务
如 Linux:手机、电脑,底层的硬件、OS、上层 App
权限管理:内核态、用户态
系统调用:App – 陷入内核态 – 访问硬件设备
2. 软中断:系统调用的入口
2.1 权限管理
- 程序的用户态、内核态
- 操作系统 + CPU 软中断:
swi / svc
- CPU 的运行级别:普通权限 & 特权
- ARM32:
- 普通模式:User
- 特权模式:FIQ、IRQ、SVC、ABT、UND
- ARM64:EL0、EL1、EL2、EL3
- X86:ring0 ~ ring3
- ARM32:
2.2 系统调用号
- 系统调用接口:read、write
- 内核中的实现:sys_read、sys_write
- 系统调用号:
- 32 位 ARM:3、4
- 64 位 ARM:0、1
- 32 位 X86:3、4
- 64 位 X86:0、1
2.4 man 2 syscall
man 2 syscall
2.5 实验:arm 系统调用
.text
.global _start_start:mov r0, #1 /* stdout*/add r1, pc, #16 /* address of the string*/mov r2, #12 /* string length*/mov r7, #4 /*syscall for 'write'*/swi #0 /* software interrupt*/_exit:mov r7, #1 /* syscall for 'exit'*/swi #0 /* software interrupt*/_string:
.asciz "Hello world\n" @ our string, NULL terminated
2.6 实验:x86 系统调用
- x86_32
.text
_start:mov $4, %eax /* syscall write */mov $1, %ebx /* fd */mov $str, %ecxmov $13, %edxint $0x80mov $1, %eaxmov $0, %ebxint $0x80.data
str:.string "Hello world!\n"
- x86_64
.text
_start:mov $1, %rax /* syscall write */mov $1, %rdi /* fd */mov $str, %rsimov $13, %rdxsyscallmov $60, %raxmov $0, %rdisyscall.data
str:.string "Hello world!\n"
3. 系统调用接口的封装
3.1 glibc
3.1.1 C 标准库
- 包含一系列系统调用接口的封装
read、write、fork、open …
3.1.2 x86 平台
3.1.3 arm 平台
3.2 syscall
- 在 C 标准库中没有封装的系统调用
- syscall 是一个库函数
- syscall 封装了系统调用的汇编接口
- 系统调用前保存 CPU 寄存器
- 从系统调用返回后,恢复寄存器
3.2.1 x86_64 下
#include <stdio.h>
#include <unistd.h>int main(void)
{printf("Hello world!\n");write(1, "Hello world!\n", 13);// 系统调用号1 标准输出 字符串内容 字符串长度syscall(1, 1, "Hello syscall!\n", 15);return 0;
}
3.2.2 arm 下
#include <stdio.h>
#include <unistd.h>int main(void)
{printf("Hello world!\n");write(1, "Hello world!\n", 13);// 系统调用号4 标准输出 字符串内容 字符串长度syscall(4, 1, "Hello syscall!\n", 15);return 0;
}
4. 系统调用流程分析
- 示例:kill
- 接口封装: /usr/arm-linux-gnueabi/lib/libc.a
- 系统调用号: arch/arm/include/generated/calls-eabi.S
- 内核实现: kernel/signal.c
- 中断处理: arch/arm/kernel/entry-common.S
// kernel\linux-5.10.4\arch\arm\include\generated\uapi\asm\unistd-common.h
...
#define __NR_kill (__NR_SYSCALL_BASE + 37) // 系统调用号
...
// kernel\linux-5.10.4\include\linux\syscalls.h
...
asmlinkage long sys_kill(pid_t pid, int sig); // 系统调用函数声明
asmlinkage long sys_tkill(pid_t pid, int sig);
asmlinkage long sys_tgkill(pid_t tgid, pid_t pid, int sig);
...// kernel\linux-5.10.4\kernel\signal.c
...
/*** sys_kill - send a signal to a process* @pid: the PID of the process* @sig: signal to be sent*/
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) // 系统调用函数实现
{struct kernel_siginfo info;prepare_kill_siginfo(sig, &info);return kill_something_info(sig, &info, pid);
}
...
// kernel\linux-5.10.4\arch\arm\include\generated\calls-eabi.S
...
NATIVE(37, sys_kill) // 中断向量表
...// kernel\linux-5.10.4\arch\arm\kernel\entry-common.S
// 中断处理程序
5. 添加一个系统调用
- 增加内核对应的实现函数
- 在系统调用表中增加一个系统调用号及入口
- 编写测试程序:在应用层发起系统调用
6. 系统调用的开销
6.1 主要开销
- 中断
- 抢占系统、任务调度
- 同步
- IO 等待
6.2 解决之道
- 快速系统调用:fast system call
- 虚拟系统调用:vsyscall
- 虚拟动态共享对象:VDSO
6.2.1 快速系统调用
- x86 处理器
- 专门为系统调用设计的汇编指令
Intel:sysenter、sysexit
AMD:syscall、sysret - 简化了系统调用和返回流程
- 预加载参数、不做权限检查
- 不再查表,直接从寄存器取值,实现快速跳转:MSR 寄存器
- 不需要保存地址和返回地址等信息
- 专门为系统调用设计的汇编指令
6.2.2 虚拟系统调用:vsyscall
proc/<PID>/maps
# 查看进程的虚拟地址空间是如何使用的。
- 编程实验:获取当前系统时间
- 使用系统调用 time 获取当前时间
- 使用虚拟系统调用接口获取当前时间
- 比较 1 和 2 的结果并分析
原先:app – time – int 80h / syscall – kernel – sys_time – user space
现在:app – time – vsyscall – user space (效率更高)
#include <stdio.h>
#include <time.h>typedef time_t (*time_fp)(time_t *);int main(void)
{time_t t1, t2;t1 = time(NULL);time_fp fp = (time_fp)0xffffffffff600400; // 可以下载 glibc-2.22 之前的版本,// 搜索:grep -nr VSYSCALL_ADDR_vtime 查看这里的地址。t2 = fp(NULL);printf("t1 = %ld\n", t1);printf("t2 = %ld\n", t2);return 0;
}
6.2.3 虚拟动态共享对象:VDSO
- vsyscall 的局限性
- 分配的内存有限
- 只支持 4 个系统调用
- 在进程中的位置是静态的、固定的,易受攻击
- VDSO 的改进
- 提供超过 4 个系统调用
- 在进程中的地址是随机的。如:time
6.2.3.1 VDSO:virtual dynamic shared object
# cat /proc/self/maps
(6.2.2 中演示)- 源码在内核中实现
arch/arm/kernel/vdso.c
关键函数:vdso_mremap、install_vvar - 速度最快
- 开销最小,基本等价于函数调用开销
6.2.3.2 VVAR:VDSO data page
struct vdso_data
6.2.3.3 实验:反汇编 VDSO 动态库
- 将 VDSO 指令从内存中 dump 出来
- 反汇编为汇编代码
- 分析汇编代码
可以关闭随机地址功能:echo 0 > /proc/sys/kernel/randomize_va_space
可以通过:cat /proc/self/smaps
查看 vdso 大小。(输出内容较多,截取 vdso 大小的部分如下)
- 将 VDSO 指令从内存中 dump 出来
- 反汇编查看:objdump -T 显示文件的动态符号表入口,仅仅对动态目标文件有意义,比如某些共享库
起始地址:0x7ffff7fcd000
偏移:0x9e0
- 分析
#include <stdio.h>
#include <time.h>typedef time_t (*time_fp)(time_t *);int main(void)
{time_t t1, t2;t1 = time(NULL);time_fp fp = (time_fp)(0x7ffff7fcd000 + 0x9e0); // 见上述2,起始地址 + 偏移t2 = fp(NULL);printf("t1 = %ld\n", t1);printf("t2 = %ld\n", t2);return 0;
}