操作系统实战 45 讲
彭东
网名 LMOS,Intel 傲腾项目关键开发者
65203 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 60 讲
尝尝鲜:从一个Hello到另一个Hello (2讲)
特别放送 (1讲)
操作系统实战 45 讲
15
15
1.0x
00:00/00:00
登录|注册

42 | 瞧一瞧Linux:如何实现系统API?

区别syscall指令和int指令
编写应用测试
编译Linux内核
定义系统调用
申明系统调用
Linux系统调用表中存放了系统调用函数的地址
Linux系统调用表的生成方式
归纳出Linux系统API框架图
库函数通过寄存器传递参数,最后执行syscall指令进入Linux内核执行系统调用
glibc库是大部分应用程序的基础
思考题
为Linux系统增加自定义系统调用
了解Linux系统中的API接口数量
了解Linux系统的API架构
总结

该思维导图由 AI 生成,仅供参考

你好,我是 LMOS。
上节课,我们通过实现一个获取时间的系统服务,学习了 Cosmos 里如何建立一个系统服务接口。Cosmos 为应用程序提供服务的过程大致是这样的:应用程序先设置服务参数,然后通过 int 指令进入内核,由 Cosmos 内核运行相应的服务函数,最后为应用程序提供所需服务。
不知道你是否好奇过业内成熟的 Linux 内核,又是怎样为应用程序提供服务的呢?
这节课我们就来看看 Linux 内核是如何实现这一过程的,我们首先了解一下 Linux 内核有多少 API 接口,然后了解一下 Linux 内核 API 接口的架构,最后,我们动手为 Linux 内核增加一个全新的 API,并实现相应的功能。
下面让我们开始吧!这节课的配套代码你可以从这里下载。

Linux 内核 API 接口的架构

在上节课中,我们已经熟悉了我们自己的 Cosmos 内核服务接口的架构,由应用程序调用库函数,再由库函数调用 API 入口函数,进入内核函数执行系统服务。
其实对于 Linux 内核也是一样,应用程序会调用库函数,在库函数中调用 API 入口函数,触发中断进入 Linux 内核执行系统调用,完成相应的功能服务。
在 Linux 内核之上,使用最广泛的 C 库是 glibc,其中包括 C 标准库的实现,也包括所有和系统 API 对应的库接口函数。几乎所有 C 程序都要调用 glibc 的库函数,所以 glibc 是 Linux 内核上 C 程序运行的基础。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Linux内核实现系统API的过程是本文的主要内容。文章首先介绍了Linux内核API接口的架构,解释了应用程序调用库函数,再由库函数调用API入口函数,触发中断进入Linux内核执行系统调用的流程。接着,文章分析了Linux内核的API接口数量,通过编译Linux代码生成系统调用号和系统调用实现函数之间的对应关系,得出Linux内核一共有441个系统调用,最后一个系统调用是sys_process_madvise。文章通过代码分析和结构示意图展示了Linux内核API的架构结构,深入浅出地介绍了Linux内核的技术特点。 此外,文章还介绍了Linux系统调用表的实现方式,以及如何为Linux增加一个系统调用的具体步骤。从下载Linux源码、申明系统调用、定义系统调用到编译Linux内核,逐步讲解了为现有的Linux写一个系统调用的过程。通过详细的步骤和代码示例,读者可以更好地理解Linux系统调用的工作原理和实现方法。 总的来说,本文通过深入的技术分析和实际操作指导,帮助读者全面了解了Linux内核实现系统API的过程,对于想深入了解Linux内核技术的读者具有很高的参考价值。文章内容涵盖了Linux内核API架构、系统调用表的生成方式以及为Linux增加系统调用的具体步骤,为读者提供了全面的技术知识和操作指导。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《操作系统实战 45 讲》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(12)

  • 最新
  • 精选
  • neohope
    置顶
    三、Demo部分 1、新建一个源码编译目录 mkdir kernelbuild 2、下载源码,解压 wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.59.tar.gz tar -xzf linux-5.10.59.tar.gz cd linux-5.10.59 3、清理 make mrproper 4、修改文件 4.1、arch/x86/entry/syscalls/syscall_64.tbl #在440后面增加一行 441 common get_cpus sys_get_cpus 4.2、include/linux/syscalls.h #在最后一个asmlinkage增加一行 asmlinkage long sys_get_cpus(void); 4.3、kernel/sys.c #在最后一个SYSCALL_DEFINE0后面增加下面几行 //获取系统中有多少CPU SYSCALL_DEFINE0(get_cpus) { return num_present_cpus(); } 5、内核配置 make menuconfig make oldconfig 6、修改.config,去掉一个证书 CONFIG_SYSTEM_TRUSTED_KEYS=“” 7、编译 make -j4 8、安装 sudo make modules_install sudo make install 9、测试 #include <stdio.h> #include <unistd.h> #include <sys/syscall.h> int main(int argc, char const *argv[]) { //syscall就是根据系统调用号调用相应的系统调用 long cpus = syscall(441); printf("cpu num is:%d\n", cpus);//输出结果 return 0; } gcc cpus.c -o cpus ./cpus 在没有修改的内核上返回是-1 在修改过的为num_present_cpus数量,我的虚拟机返回的是1

    作者回复: 66666大佬 大佬

    2021-08-17
    10
  • neohope
    二、linux内核部分【下】 2、当产生系统调用时 2.1、应用直接syscall或通过glibc产生了syscall 2.2、cpu会产生类似于中断的效果,开始到entry_SYSCALL_64执行 //文件路径arch/x86/entry/entry_64.S SYM_CODE_START(entry_SYSCALL_64) //省略代码 call do_syscall_64 SYM_CODE_END(entry_SYSCALL_64) //文件路径arch/x86/entry/entry_64.S SYM_CODE_START(entry_SYSCALL_compat) call do_fast_syscall_32 SYM_CODE_END(entry_SYSCALL_compat) 2.3、调用do_syscall_64 #ifdef CONFIG_X86_64 __visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs) { nr = syscall_enter_from_user_mode(regs, nr); instrumentation_begin(); if (likely(nr < NR_syscalls)) { nr = array_index_nospec(nr, NR_syscalls); regs->ax = sys_call_table[nr](regs); } instrumentation_end(); syscall_exit_to_user_mode(regs); } #endif 2.4、根据sys_call_table调用对应的功能函数 sys_call_table[nr](regs) 如果我们传入257,就会调用__x64_sys_openat 如果我们传入441,就会调用__x64_sys_get_cpus 2.5、但咱们实际写的函数sys_get_cpus,好像和实际调用函数__x64_sys_get_cpus,差了一个__x64,这需要一个wrapper arch\x86\include\asm\syscall_wrapper.h #define SYSCALL_DEFINE0(sname) \ SYSCALL_METADATA(_##sname, 0); \ static long __do_sys_##sname(const struct pt_regs *__unused); \ __X64_SYS_STUB0(sname) \ __IA32_SYS_STUB0(sname) \ static long __do_sys_##sname(const struct pt_regs *__unused) #define __X64_SYS_STUB0(name) \ __SYS_STUB0(x64, sys_##name) #define __SYS_STUB0(abi, name) \ long __##abi##_##name(const struct pt_regs *regs); \ ALLOW_ERROR_INJECTION(__##abi##_##name, ERRNO); \ long __##abi##_##name(const struct pt_regs *regs) \ __alias(__do_##name); SYSCALL_DEFINE0(get_cpus),会展开成为 __X64_SYS_STUB0(get_cpus) 然后 __SYS_STUB0(x64, sys_get_cpus) 然后 long __x64_sys_get_cpus(const struct pt_regs *regs); 这样前后就对上了,glibc和linux内核就通了。

    作者回复: 是的

    2021-08-17
    5
  • neohope
    一、glibc部分【上】 1、应用程序调用open函数 //glibc/intl/loadmsgcat.c # define open(name, flags) __open_nocancel (name, flags) 2、展开后实际上调用了 __open_nocancel(name, flags) 3、而__open_nocancel 最终调用了INLINE_SYSCALL_CALL //glibc/sysdeps/unix/sysv/linux/open_nocancel.c __open_nocancel(name, flags) ->return INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode); 4、宏展开【理解就好,不保证顺序】 4.1、初始为 INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode); 4.2、第1次展开INLINE_SYSCALL_CALL #define INLINE_SYSCALL_CALL(...) \ __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__) 展开得到: __INLINE_SYSCALL_DISP(__INLINE_SYSCALL, __VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】) 4.3、第2次展开__INLINE_SYSCALL_DISP #define __INLINE_SYSCALL_DISP(b,...) \ __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__) 展开得到: __SYSCALL_CONCAT(b【__INLINE_SYSCALL】,__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】))(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】) 4.4、第3次展开__INLINE_SYSCALL_NARGS __INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】) #define __INLINE_SYSCALL_NARGS(...) \ __INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,) 展开得到: __INLINE_SYSCALL_NARGS_X(openat, AT_FDCWD, file, oflag, mode,7,6,5,4,3,2,1,0,) 然后展开__INLINE_SYSCALL_NARGS_X #define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n 展开得到参数个数:4 从而4.4的结果为 __SYSCALL_CONCAT(__INLINE_SYSCALL,4)(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】) 4.5、然后展开__SYSCALL_CONCAT,其实就是字符拼接 __SYSCALL_CONCAT(__INLINE_SYSCALL,4) #define __SYSCALL_CONCAT_X(a,b) a##b #define __SYSCALL_CONCAT(a,b) __SYSCALL_CONCAT_X (a, b) 展开得到: __INLINE_SYSCALL4 从而4.5的结果为 __INLINE_SYSCALL4(openat, AT_FDCWD, file, oflag, mode) 4.6、然后展开INTERNAL_SYSCALL4 #define __INLINE_SYSCALL4(name, a1, a2, a3, a4) \ INLINE_SYSCALL (name, 4, a1, a2, a3, a4) 展开得到: INLINE_SYSCALL(openat, 4, AT_FDCWD, file, oflag, mode)

    作者回复: 对的 就是这样

    2021-08-17
    4
  • neohope
    二、linux内核部分【上】 1、在make时,会通过syscall_64.tbl生成syscalls_64.h,然后包含到syscall_64.c,进行调用号与函数之间的绑定。 arch/x86/entry/syscalls/syscall_64.tbl arch/x86/include/generated/asm/syscalls_64.h arch/x86/entry/syscall_64.c 1.1、以sys_openat为例,在syscall_64.tbl中为 257 common openat sys_openat 441 common get_cpus sys_get_cpus 1.2、make后,在生成的syscalls_64.h中为 __SYSCALL_COMMON(257, sys_openat) 1.3 在syscall_64.c中,展开__SYSCALL_COMMON #define __SYSCALL_COMMON(nr, sym) __SYSCALL_64(nr, sym) 展开就是 __SYSCALL_64(257, sys_openat) 1.4、在syscall_64.c中,第一次展开__SYSCALL_64 #define __SYSCALL_64(nr, sym) extern long __x64_##sym(const struct pt_regs *); #include <asm/syscalls_64.h> #undef __SYSCALL_64 展开就是 extern long __x64_sys_openat(const struct pt_regs *); 也就是每个__SYSCALL_64都展开成了一个外部函数 1.5、在syscall_64.c中,第二次展开__SYSCALL_64 #define __SYSCALL_64(nr, sym) [nr] = __x64_##sym, asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { [0 ... __NR_syscall_max] = &__x64_sys_ni_syscall, #include <asm/syscalls_64.h> }; 展开其实就是指向了外部函数 [257]=__x64_sys_openat, 全部展开结果,都会被包含到sys_call_table中,从而完成了调用号与函数之间的绑定。

    作者回复: 是的 完全正确

    2021-08-17
    3
  • cugphoenix
    看了Linux 系统调用表的生成方式,又刷新了对宏定义的认知,灵活性极强,用起来可太花哨了

    作者回复: 是的

    2021-08-14
    2
  • Zhendicai
    int指令和syscall指令都会发生特权级切换吧,但是syscall能直接调定位到具体系统调用函数,int则需要经过中断门描述符表和分发器才行。int的步骤要多一些,那就是取指令次数要多一些?还有个问题使用int来执行系统调用是不是也会遇到中断优先级的问题?

    作者回复: 是的正确

    2021-08-13
    2
  • 吴建平
    有个疑问,最后验证的应用例子,怎么链接到系统调用的呢,没看到链接过程,默认链接glibc么?

    作者回复: 要编译内核的

    2022-03-29
    1
  • 凉人。
    搬了下答案 1.syscall syscall是x64的系统调用。其调用号通过rax进行传递。查看具体的调用号,linux环境下在unistd.h中定义。如果是64位,则可以查看/usr/include/asm/unistd_64.h,如果是32位,则查看/usr/include/unistd_32.h。 参数传递:处于用户态时,参数传递顺序为:rdi,rsi,rdx,rcx,r8,r9,处于内核态时,参数传递顺序:rdi,rsi,rdx,r10,r8,r9(补充:这是看别人文章是这么写的,但是我在实际操作中发现,我运行用户态汇编代码,通过rcx传递参数时函数返回错误,用ida查看,发现用户态的值传递其实是:rdi,rsi,rdx,r10,r8,r9(和前面提到的内核态的一致),内核的值传递我没有进行测试,欢迎各位大佬评论区补充) 2.int 80h int 80h 是32位x86的系统调用方式。同样通过ax传递调用号,参数传递顺序是:ebx,ecx,edx,esi,edi *** note *** intel体系的系统调用限制最多六个参数,没有任何一个参数是通过栈传递的。系统调用的返回结果存放在ax寄存器中,且只有整型和内存型可以传递给内核

    作者回复: 是的 正确

    2021-08-13
    1
  • 罗 乾 林
    因为这里用到的指令是最新处理器为其设计的系统调用指令 syscall。这个指令和 int 指令一样,都可以让 CPU 跳转到特定的地址上,只不过不经过中断门,系统调用返回时要用 sysexit 指令

    作者回复: 对的

    2021-08-13
  • pedro
    实验太顶了,有时间一定搞一下,今天问题属于知识盲区,贴上链接:https://blog.csdn.net/sdulibh/article/details/50890250 不学习就变废物😂

    作者回复: 哈哈 这有点费时 没什么 难度

    2021-08-13
收起评论
显示
设置
留言
12
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部