[内核学习]系统调用机制

/ 0评 / 0赞

系统调用是用户态和内核唯一的正门:一次受控的特权级切换,带着编号和参数进门,办完事拿结果出去.1.1 讲过 EL 分层,1.3 讲过异常向量,这篇把这条路径彻底走通 — 包括真的给内核加一个自己的系统调用,从修改表到用户态调通,一步不缺.

这篇回答几件事:

先把硬件侧流程串起来,大部分在 1.3 预习过:用户态执行 svc #0,硬件切到 EL1,跳进向量表的 el0_sync 槽,ESR 的 EC=0x15(SVC),分发到 el0_svc,再进 arch/arm64/kernel/syscall.c 的 el0_svc_common.分发那一行很短:

	el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);

规矩全在这:AARCH64 的系统调用号放在 x8,参数最多六个放 x0 到 x5,返回值 x0.没有栈传参这一说,超过六个参数的系统调用极少,真要多就打包成结构体传指针.表里查不到号,或者表项是空,统一 ENOSYS(Function not implemented)— 这个 errno 专门为"号不存在"保留.

号表本身:arm64 的系统调用表在 6.18 是从 arch/arm64/tools/syscall_64.tbl 生成的,文本表一行一个 — 编号,ABI 分类,名字,实现函数.scripts/syscallhdr.sh 把它编译成 unistd_64.h,内核和用户态的 C 头文件同源,所以两边永远不会对不上号.看几个眼熟的号:openat 是 56,close 是 57,newfstatat 是 79,号本身没有规律,纯粹按加入顺序排 — 这也是不同架构系统调用号不同的原因,x86_64 的 openat 是 257,写跨平台代码永远用 glibc 的包装函数,别写裸数字.

观测这块,板上直接用 tracepoint:

bash-1506 [001] sys_enter: NR 24 (a, 1, 0, 9, 8, 0)
bash-1506 [001] sys_enter: NR 57 (a, 0, 0, 0, 0, 0)
bash-1506 [001] sys_enter: NR 79 (ffffffffffffff9c, aaaae76e9ab0, ...)

raw_syscalls/sys_enter 这个埋点把号和六个参数原样吐出来,跑一个 /bin/true 就抓到一小串.第三行 NR 79(newfstatat)第一个参数是 0xffffffffffffff9c,即 -100 — AT_FDCWD,相对路径解析的"当前目录"句柄.内核里翻成无符号打印,负数就长这样,读 trace 时要有这个手感.strace 干的也是同一件事,不过它把号翻译成名字和结构体字段,日常用 strace,讲机制用 tracepoint,正好互补.

然后是这篇的主菜:自己加一个系统调用.目标很朴素,叫 kblog_info,把一串内核生成的文本拷给用户.三步.第一步写实现,kernel/sys.c 末尾:

SYSCALL_DEFINE3(kblog_info, char __user *, buf, size_t, len, int __user *, out)
{
	char msg[64];
	int n = snprintf(msg, sizeof(msg), "kblog syscall alive, boot=%lds",
			 ktime_get_boottime_seconds());
	if (len < n)
		n = len;
	if (copy_to_user(buf, msg, n))
		return -EFAULT;
	if (put_user(462, out))
		return -EFAULT;
	return n;
}

SYSCALL_DEFINE 宏展开成 sys_kblog_info 函数加类型检查桩,参数自带 __user 标记.第二步注册,syscall_64.tbl 末尾加一行:

470	common	kblog_info		sys_kblog_info

第三步重编内核,QEMU 起来,用户态直接 syscall(470, ...):

syscall(470) -> 28: kblog syscall alive, boot=4s, magic=462

通了.参数进去,字符串出来,出参 magic 也到位,一条不到三十行的链路把"用户态 C 函数 → glibc 的 syscall 桩 → svc 指令 → 向量表 → el0_svc_common → 按号查表 → 我的函数 → 原路返回"全部点亮.这份体验平时只有 libc 和内核开发者有,自己动手一次,整条路径从此是熟人.

顺带交代翻的车.我一开始照着老教程把 __NR 定义补在 include/uapi/asm-generic/unistd.h,编译启动一切正常,调用返回 ENOSYS — 表里根本没有我.查生成的头文件才发现,arm64 早就迁到自己的 syscall_64.tbl,asm-generic 那份压根不参与编译.教材和现实差着一个架构迁移,这是内核学习反复出现的主题:文章教你机制,树里教你现实,动手之前先看 make 的 .cmd 和 generated 目录,确认这份源码到底吃哪份配置.

最后留几个容易忽略的细节:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注