Kernel Exploring
  • 前言
  • 支持
  • 老司机带你探索内核编译系统
    • 编译出你的第一个内核
    • 内核编译中的小目标
    • 可能是kbuild中最直接的小目标 – help
    • 使用了一个kbuild函数的目标 – cscope
    • 内核中单个.o文件的编译过程
    • 根目录vmlinux的编译过程
    • 启动镜像bzImage的前世今生
    • setup.bin的诞生记
    • 真假vmlinux–由vmlinux.bin揭开的秘密
    • bzImage的全貌
    • kbuild系统浅析
  • 启动时的小秘密
    • INIT_CALLS的秘密
    • 内核参数
  • 内核加载全流程
    • bootloader如何加载bzImage
    • 内核压缩与解压
    • 内核加载的几个阶段
    • 保护模式内核代码赏析
  • 内存管理
    • 内核页表成长记
      • 未解压时的内核页表
      • 内核早期的页表
      • cleanup_highmap之后的页表
      • 映射完整物理地址
      • 启用init_level4_pgt
    • 自底而上话内存
      • e820从硬件获取内存分布
      • 原始内存分配器--memblock
      • 页分配器
        • 寻找页结构体的位置
        • 眼花的页结构体
        • Node-Zone-Page
        • 传说的伙伴系统
        • Compound Page
        • GFP的功效
        • 页分配器的用户们
      • slub分配器
        • slub的理念
        • 图解slub
      • 内存管理的不同粒度
      • 挑战和进化
        • 扩展性的设计和实现
        • 减少竞争 per_cpu_pageset
        • 海量内存
        • 延迟初始化
        • 内存热插拔
        • 连续内存分配器
    • 虚拟内存空间
      • 页表和缺页中断
      • 虚拟地址空间的管家--vma
      • 匿名反向映射的前世今生
      • 图解匿名反向映射
      • THP和mapcount之间的恩恩怨怨
      • 透明大页的玄机
      • NUMA策略
      • numa balance
      • 老版vma
    • 内存的回收再利用
      • 水线
      • Big Picture
      • 手动触发回收
      • Page Fram Reclaim Algorithm
      • swapfile原理使用和演进
    • 内存隔离
      • memcg初始化
      • 限制memcg大小
      • 对memcg记账
    • 通用
      • 常用全局变量
      • 常用转换
    • 测试
      • 功能测试
      • 性能测试
  • 中断和异常
    • 从IDT开始
    • 中断?异常?有什么区别
    • 系统调用的实现
    • 异常向量表的设置
    • 中断向量和中断函数
    • APIC
    • 时钟中断
    • 软中断
    • 中断、软中断、抢占和多处理器
  • 设备模型
    • 总线
    • 驱动
    • 设备
    • 绑定
  • nvdimm初探
    • 使用手册
    • 上帝视角
    • nvdimm_bus
    • nvdimm
    • nd_region
    • nd_namespace_X
    • nd_dax
      • dev_dax
  • KVM
    • 内存虚拟化
      • Qemu内存模型
      • KVM内存管理
  • cgroup
    • 使用cgroup控制进程cpu和内存
    • cgroup文件系统
    • cgroup层次结构
    • cgroup和进程的关联
    • cgroup数据统计
  • 同步机制
    • 内存屏障
    • RCU
  • Trace/Profie/Debug
    • ftrace的使用
    • 探秘ftrace
    • 内核热补丁的黑科技
    • eBPF初探
    • TraceEvent
    • Drgn
  • 内核中的数据结构
    • 双链表
    • 优先级队列
    • 哈希表
    • xarray
    • B树
    • Maple Tree
    • Interval Tree
  • Tools
  • Good To Read
    • 内核自带文档
    • 内存相关
    • 下载社区邮件
Powered by GitBook
On this page
  • 从调用方式开始
  • 从前的调用方式
  • 现在的调用方式
  • 从syscall到sys_call_table
  • sys_call_table的构造
  • 最后一环

Was this helpful?

  1. 中断和异常

系统调用的实现

系统调用是用户程序和内核之间沟通的桥梁,没有他内核跑起来也根本用不上。不过我们好像从来就没有正眼看过他。今天我们就来瞧瞧这位一直默默付出的幕后英雄。

从调用方式开始

以前在课本上我们学到系统调用是通过int 0x80来实现的。也就是用户程序通过IDT 0x80的这个中断向量和内核发生联系。只不过那已经是很久以前的事情了。温故而知新,那就从新旧两种调用方式入手吧。

从前的调用方式

.data                   # section declaration

msg:
	.string "Hello, world!\n"
	len = . - msg   # length of our dear string

.text                   # section declaration

                        # we must export the entry point to the ELF linker or
  .global main          # loader. They conventionally recognize _start as their
                        # entry point. Use ld -e foo to override the default.

main:

# write our string to stdout

	movl    $len,%edx   # third argument: message length
	movl    $msg,%ecx   # second argument: pointer to message to write
	movl    $1,%ebx     # first argument: file handle (stdout)
	movl    $4,%eax     # system call number (sys_write)
	int     $0x80       # call kernel

# and exit

	movl    $0,%ebx     # first argument: exit code
	movl    $1,%eax     # system call number (sys_exit)
	int     $0x80       # call kernel

可以看到,这种方式就是通过int 0x80来实现的。这段代码在x86平台上还能运行,用如下方式进行编译:

# gcc -c hello-ia32.s
# ld -e main -o hell hello-ia32.o

现在的调用方式

接下来看看新的方式:

.data

msg:
	.ascii "Hello World!\n"
	len = . - msg

.text
	.global _start

_start:
	movq $1, %rax
	movq $1, %rdi
	movq $msg, %rsi
	movq $len, %rdx
	syscall

	movq $60, %rax
	xorq %rdi, %rdi
	syscall

怎么样,看着是不是没啥大区别?关键是这里使用的是syscall这个指令来完成系统调用。既然如此,那就从这个指令开始吧。

从syscall到sys_call_table

了解了当前系统调用的实现方式,接着我们就想要了解它是如何同内核中的系统调用函数结合的,如何一步步调用到那些熟悉的系统调用函数的。

想要了解syscall这个指令这一切还要从手册开始。在SDM Volume 2中就有这个指令的详尽解释,这里摘抄一段。

SYSCALL invokes an OS system-call handler at privilege level 0. It does so by loading RIP from the IA32_LSTAR MSR (after saving the address of the instruction following SYSCALL into RCX).

原来这次不是通过IDT找到跳转地址,而是通过了一个msr来保存。而这个msr在内核中定义为 MSR_LSTAR。由此我们找到了内核中这段代码:

  wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

在entry_SYSCALL_64这段汇编中,又看到了这么一行醒目的代码:

  call	do_syscall_64		/* returns with IRQs disabled */

当我们进一步打开这个函数时,一切或许就有些明朗了:

__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
	struct thread_info *ti;

	enter_from_user_mode();
	local_irq_enable();
	ti = current_thread_info();
	if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
		nr = syscall_trace_enter(regs);

	/*
	 * NB: Native and x32 syscalls are dispatched from the same
	 * table.  The only functional difference is the x32 bit in
	 * regs->orig_ax, which changes the behavior of some syscalls.
	 */
	nr &= __SYSCALL_MASK;
	if (likely(nr < NR_syscalls)) {
		nr = array_index_nospec(nr, NR_syscalls);
		regs->ax = sys_call_table[nr](regs);
	}

	syscall_return_slowpath(regs);
}

在函数中最后一段的if语句中,可以看到我们通过nr索引了sys_call_table的相关项并调用。

这就是我们要找的系统调用函数表了。

由此我们一路走来,终于找到了目标。在接着往下探索之前,先来回顾一下我们是怎么走到这里的。

syscall -> entry_SYSCALL_64 -> do_syscall_64 -> sys_call_table

sys_call_table的构造

走到了这,基本上万里长征完成了大半。接下来就是查看这张表是如何构成的。

这个就是sys_call_table的定义了。

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

这个定义中的关键就在最后那个包含的头文件中,而这个文件是编译时生成的

./arch/x86/include/generated/asm/syscalls_64.h

生成的规则是

syscall64 := $(srctree)/$(src)/syscall_64.tbl
systbl := $(srctree)/$(src)/syscalltbl.sh

quiet_cmd_systbl = SYSTBL  $@
      cmd_systbl = $(CONFIG_SHELL) '$(systbl)' $< $@

$(out)/syscalls_64.h: $(syscall64) $(systbl)
	$(call if_changed,systbl)

如果可以用一个为代码表示这个过程的话,那就是

syscalls_64.h = syscalltbl.sh (syscall_64.tbl)

所以这个头文件syscalls_64.h是脚本syscalltbl.sh处理文件syscall_64.tbl的结果。

当你打开syscall_64.tbl的时候,会有种云开雾散的感觉。

#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0	common	read			__x64_sys_read
1	common	write			__x64_sys_write
2	common	open			__x64_sys_open
3	common	close			__x64_sys_close

这里就保存这整个系统调用的对应关系。

看到了生成头文件的原料,那来看看生成的头文件是什么样子。打开生成的syscalls_64.h,看到syscall_64.tbl中的每一行都展开成如下的形式:

#ifdef CONFIG_X86
__SYSCALL_64(0, __x64_sys_read, )
#else /* CONFIG_UML */
__SYSCALL_64(0, sys_read, )
#endif

而这个__SYSCALL_64的宏定义如下:

#define __SYSCALL_64(nr, sym, qual) [nr] = sym,

这样可能还不是很清楚,我们再把这个代入到sys_call_table的定义中再看一眼。

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
    [0] = __x64_sys_read,
    [1] = __x64_sys_write,
    [2] = __x64_sys_open,
    ...
    ...
    ...
};

最后一环

表也有了,现在就差最后一环了,函数__x64_sys_read()在哪里?这事我们还得倒过来看。

正着找找不到,那我们就反过来找。从read系统调用的定义入手。

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
	return ksys_read(fd, buf, count);
}

这其中有个宏SYSCALL_DEFINE3。

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

最后的最后,还有一个__SYSCALL_DEFINEx。

#define __SYSCALL_DEFINEx(x, name, ...)					\
	asmlinkage long __x64_sys##name(const struct pt_regs *regs);	\
	ALLOW_ERROR_INJECTION(__x64_sys##name, ERRNO);			\
	static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	asmlinkage long __x64_sys##name(const struct pt_regs *regs)	\
	{								\
		return __se_sys##name(SC_X86_64_REGS_TO_ARGS(x,__VA_ARGS__));\
	}								\
	__IA32_SYS_STUBx(x, name, __VA_ARGS__)				\
	static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

这玩意确实有点长,不过你是不是看到了__x64_sys##name的字样呢?

好了,我相信你已经懂了。在sys_call_table表中填入的函数入口,就是由__SYSCALL_DEFINEx定义出来的。

经过了一番探索,我们终于理清了系统调用在linux中是如何定义和关联的。

Previous中断?异常?有什么区别Next异常向量表的设置

Last updated 3 years ago

Was this helpful?