手把手教你在Linux内核中添加一个系统调用

实现一个专属系统调用

  最近在研究Linux内核的内存管理,书上说进程所使用的虚拟地址信息全部保存在vm_area_struct结构体中,并未给出实例,这个结构体也是在内核空间的,所以用户空间是不能直接访问的,正好最近看到系统调用这一章节,于是想到像内核添加一个自己的系统调用,用来打印当前进程的task_struct进程控制块中我自己关注的任何信息。

实现专属系统调用

  在fs目录下新建一个名为panglib.c的文件,我们要新添加的系统调用就在这个c文件里实现,除了新建panglib.c文件,我们还需要修改fs目录下的Makefile文件,如下图所示,将panglib.c添加到内核的编译规则。
在这里插入图片描述

实现系统调用

  panglib.c的内容如下,系统调用名为sys_pang。

#include <linux/string.h>
#include <linux/mm.h>
#include <linux/file.h>
#include <linux/fdtable.h>
#include <linux/fsnotify.h>
#include <linux/module.h>
#include <linux/tty.h>
#include <linux/namei.h>
#include <linux/backing-dev.h>
#include <linux/capability.h>
#include <linux/securebits.h>
#include <linux/security.h>
#include <linux/mount.h>
#include <linux/fcntl.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/fs.h>
#include <linux/personality.h>
#include <linux/pagemap.h>
#include <linux/syscalls.h>
#include <linux/rcupdate.h>
#include <linux/audit.h>
#include <linux/falloc.h>
#include <linux/fs_struct.h>
#include <linux/ima.h>
#include <linux/dnotify.h>
#include <linux/compat.h>

#include "internal.h"

static void print_vm_area(void)
{
        struct mm_struct *mymm =  current->mm;
        struct task_struct *task = NULL;
    struct vm_area_struct *pos = NULL;

    printk("hello \n");

    printk("current process:%s %d\n", current->comm, current->tgid);
                #if 1
    for(pos = mymm->mmap; pos; pos = pos->vm_next) {
        printk("0x%lx-0x%lx\t", pos->vm_start, pos->vm_end);
        if(pos->vm_flags & VM_READ) {
            printk("r");
        } else {
            printk("-");
        }
        if(pos->vm_flags & VM_WRITE) {
            printk("w");
        } else {
            printk("-");
        }
        if(pos->vm_flags & VM_EXEC) {
            printk("x");
        } else {
            printk("-");
        }

        printk("\n");
    }
        #endif
    return 0;

}

SYSCALL_DEFINE2(pang, const char __user *,buf,size_t, count)
{

	print_vm_area();
	return 0;
}

谈一谈SYSCALL_DEFINE2宏

  展开后如下:

			asmlinkage long sys_pang(__MAP(2,__SC_DECL,__VA_ARGS__))	\
					__attribute__((alias(__stringify(SyS_pang))));		\
					static inline long SYSC_pang(__MAP(2,__SC_DECL,__VA_ARGS__));	\
					asmlinkage long SyS_pang(__MAP(2,__SC_LONG,__VA_ARGS__));	\
					asmlinkage long SyS_pang(__MAP(2,__SC_LONG,__VA_ARGS__))	\
					{								\
					long ret = SYSC_pang(__MAP(2,__SC_CAST,__VA_ARGS__));	\
					__MAP(2,__SC_TEST,__VA_ARGS__);				\
					__PROTECT(2, ret,__MAP(2,__SC_ARGS,__VA_ARGS__));	\
					return ret;						\
					}								\
					static inline long SYSC_pang(__MAP(2,__SC_DECL,__VA_ARGS__))
                   {
                   	    print_vm_area();
	                    return 0;
                   }
                    

将专属系统调用添加到Linux内核

修改arch/arm/kernel/calls.S文件

  如下图所示,在文件的末尾新增一行 CALL(sys_pang)。
在这里插入图片描述

  这个CALL是一个宏定义,该宏定义的声明位于arch/arm/kernel/ entry-common.S文件,如下图所示。
在这里插入图片描述

CALL宏的第一次声明

  宏定义一共声明了两次,其中我们的calls.S使用的是第一个声明,该声明并未使用CALL的传参,而是使用NR_syscalls变量进行赋值然后对NR_syscalls变量进行了加一操作,当calls.S展开完成后第一个CALL就被取消声明然后重新定义了,也就是说第一次声明的CALL宏只是为了统计系统调用的个数,但是注意:在calls.S的结尾的对齐处理。

CALL宏的第二次声明与sys_call_table数组

  所有的系统调用全部保存在一个数组里,数组的每一个元素都是指向一个系统调用的函数指针,这个数组名为sys_call_table,对数组的赋值操作如下图所示:
在这里插入图片描述

  这里又再一次展开了calls.S,但是这次用的宏为:#define CALL(x) .long x,比如我们刚刚在calls.S中新增的系统调用CALL(sys_pang)会被展开为 .long sys_pang,sys_pang是我们在panglib.c中实现的系统调用的函数地址。
  到这里你应该也明白了,所谓的系统调用号说的糙一点就是sys_xxx函数在sys_call_table数组中的索引号。

修改sys_ni.c

  如下图所示,在文件的末尾新增一行 cond_syscall(sys_pang)。
在这里插入图片描述

  cond_syscall宏展开如下

#define cond_syscall(x)	asm(				\
	".weak " VMLINUX_SYMBOL_STR(x) "\n\t"		\
	".set  " VMLINUX_SYMBOL_STR(x) ","		\
		 VMLINUX_SYMBOL_STR(sys_ni_syscall))

  cond_syscall(sys_pang);语句的意思是:如果存在sys_pang(),则声明这个函数,在程序链接的时候使用这个函数;如果不存在sys_socketcall()这个函数,就使用sys_ni_syscall()函数代替。

  你可能要问了,sys_ni_syscall()是何许人也,看下函数实现你就明白了…,相当于告诉用户层你调用的系统调用在内核里并未实现。

asmlinkage long sys_ni_syscall(void)
{
	return -ENOSYS;
}

修改include/linux/syscalls.h

在这里插入图片描述

修改arch/arm/include/asm/unistd.h

在这里插入图片描述

C应用程序使用系统调用

  实验平台为Ti的ARM32处理器。

通过syscall函数访问sys_pang

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

int main()
{
        char buf[2];
        syscall(388,buf,2);
        return 0;
}

运行

在这里插入图片描述

汇编程序使用系统调用

  相比C语言,汇编更能让我们深入理解系统调用的本质。

通过swi指令陷入内核态

.text
.global _start

_start:
        add r0,pc,#0
        mov r1,#12
        mov r7,#388
        swi #0

_exit:
        mov r7,#1
        swi #0

  应用层通过swi指令触发软中断,swi的中断服务程序位于arch/arm/kernel/entry-common.S文件,源码如下:

/*=============================================================================
 * SWI handler
 *-----------------------------------------------------------------------------
 */

	.align	5
ENTRY(vector_swi)
#ifdef CONFIG_CPU_V7M
	v7m_exception_entry
#else
	sub	sp, sp, #S_FRAME_SIZE
	stmia	sp, {r0 - r12}			@ Calling r0 - r12
 ARM(	add	r8, sp, #S_PC		)
 ARM(	stmdb	r8, {sp, lr}^		)	@ Calling sp, lr
 THUMB(	mov	r8, sp			)
 THUMB(	store_user_sp_lr r8, r10, S_SP	)	@ calling sp, lr
	mrs	r8, spsr			@ called from non-FIQ mode, so ok.
	str	lr, [sp, #S_PC]			@ Save calling PC
	str	r8, [sp, #S_PSR]		@ Save CPSR
	str	r0, [sp, #S_OLD_R0]		@ Save OLD_R0
#endif
	zero_fp
	alignment_trap r10, ip, __cr_alignment
	enable_irq
	ct_user_exit
	get_thread_info tsk

	/*
	 * Get the system call number.
	 */

#if defined(CONFIG_OABI_COMPAT)

	/*
	 * If we have CONFIG_OABI_COMPAT then we need to look at the swi
	 * value to determine if it is an EABI or an old ABI call.
	 */
#ifdef CONFIG_ARM_THUMB
	tst	r8, #PSR_T_BIT
	movne	r10, #0				@ no thumb OABI emulation
 USER(	ldreq	r10, [lr, #-4]		)	@ get SWI instruction
#else
 USER(	ldr	r10, [lr, #-4]		)	@ get SWI instruction
#endif
 ARM_BE8(rev	r10, r10)			@ little endian instruction

#elif defined(CONFIG_AEABI)

	/*
	 * Pure EABI user space always put syscall number into scno (r7).
	 */
#elif defined(CONFIG_ARM_THUMB)
	/* Legacy ABI only, possibly thumb mode. */
	tst	r8, #PSR_T_BIT			@ this is SPSR from save_user_regs
	addne	scno, r7, #__NR_SYSCALL_BASE	@ put OS number in
 USER(	ldreq	scno, [lr, #-4]		)

#else
	/* Legacy ABI only. */
 USER(	ldr	scno, [lr, #-4]		)	@ get SWI instruction
#endif

	adr	tbl, sys_call_table		@ load syscall table pointer

#if defined(CONFIG_OABI_COMPAT)
	/*
	 * If the swi argument is zero, this is an EABI call and we do nothing.
	 *
	 * If this is an old ABI call, get the syscall number into scno and
	 * get the old ABI syscall table address.
	 */
	bics	r10, r10, #0xff000000
	eorne	scno, r10, #__NR_OABI_SYSCALL_BASE
	ldrne	tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
	bic	scno, scno, #0xff000000		@ mask off SWI op-code
	eor	scno, scno, #__NR_SYSCALL_BASE	@ check OS number
#endif

local_restart:
	ldr	r10, [tsk, #TI_FLAGS]		@ check for syscall tracing
	stmdb	sp!, {r4, r5}			@ push fifth and sixth args

	tst	r10, #_TIF_SYSCALL_WORK		@ are we tracing syscalls?
	bne	__sys_trace

	cmp	scno, #NR_syscalls		@ check upper syscall limit
	adr	lr, BSYM(ret_fast_syscall)	@ return address
	ldrcc	pc, [tbl, scno, lsl #2]		@ call sys_* routine

	add	r1, sp, #S_OFF
2:	cmp	scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
	eor	r0, scno, #__NR_SYSCALL_BASE	@ put OS number back
	bcs	arm_syscall
	mov	why, #0				@ no longer a real syscall
	b	sys_ni_syscall			@ not private func

#if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
	/*
	 * We failed to handle a fault trying to access the page
	 * containing the swi instruction, but we're not really in a
	 * position to return -EFAULT. Instead, return back to the
	 * instruction and re-enter the user fault handling path trying
	 * to page it in. This will likely result in sending SEGV to the
	 * current task.
	 */
9001:
	sub	lr, lr, #4
	str	lr, [sp, #S_PC]
	b	ret_fast_syscall
#endif
ENDPROC(vector_swi)

编译链接汇编文件

arm-linux-gnueabihf-as syscall.s -o syscall.o
arm-linux-gnueabihf-ld syscall.o -o syscall

运行

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
对于一个300字的回答来说,涵盖整个 Linux 内核调度器的详细步骤是不太可能的。然而,我可以给你一个大致的概述,让你了解一些关键步骤。 首先,要开始编写一个 Linux 内核调度器,你需要对操作系统的基本原理有一定的理解。你需要了解进程与线程的概念,以及它们在操作系统的调度和执行。你还需要了解调度算法以及其在进程选择方面的应用。 其次,你需要研究 Linux 内核调度器的源代码。这将涉及到理解 Linux 内核的整体结构以及调度器在其的位置和功能。你可以从 Linux 的官方源代码仓库获取这些信息。 一旦你了解了基本原理和调度器的源代码,你可以根据你的需求进行修改或编写新的调度器。这可能包括更改调度算法、优化调度策略或添加新的功能。你可能还需要做一些基准测试,以确保你的调度器在不同的负载情况下表现良好。 最后,你需要将你的调度器集成到 Linux 内核,并进行测试和验证。这可能涉及到构建和安装整个 Linux 内核,然后在实际系统运行调度器以进行测试。你可能还需要一些调试工具来帮助你找出任何问题并进行修复。 总结起来,编写一个 Linux 内核调度器是一个复杂的过程,需要深入了解操作系统原理和 Linux 内核的工作机制。这只是一个概述,涉及的步骤远远超过300字的限制。希望这个简短的回答能够给你提供一些指导。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明故宫的记忆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值