linux实践——内核编程 基础模块
一、内核模块的概念
Linux模块(module)是一些可以作为独立程序来编译的函数和数据类型的集合。内核模块给我们带来的便利是模块本身并不被编译进内核文件,可在内核运行期间动态的安装或卸载。因为如果将模块编译进内核的话,一是生产的内核文件过大,二是如果要添加或删除某个组件要重新编译整个内核。
Linux模块可以通过静态或动态地加载到内核空间,静态加载是指在内核启动过程中加载;动态加载是指在内核运行的过程中随时加载。我采用的是动态加载的方法。
一个模块被加载到内核中时,它就成为内核代码的一部分,与其他内核代码地位是一样的。模块加载如系统时,系统修改内核中的符号表,将新加载的模块提供的资源和符号加到内核符号表中,这样使模块间可进行通信。
二、内核模块的基本结构
linux内核模块的程序结构有:模块加载函数(必须),模块卸载函数(必须),模块许可证声明(必须),模块参数(可选),模块导出符号(可选),模块作者的等信息声明(可选)。
一个内核模块应该至少包含两个函数。一个“开始”(初始化)的函数被称为init_module(),当内核模块被insmod 加载时被执行,还有一个“结束”(要完成与模块加载函数相反的功能)的函数被称为cleanup_module() ,当内核模块被rmmod 卸载时被执行。实际上,从内核版本2.3.13 开始我们就可以为开始和结束函数起任意的名字了。这可以通过宏module_init()和module_exit()实现,需要注意的地方是函数必须在宏的使用前定义,否则会有编译错误。
模块许可证声明描述内核模块的许可权限,格式为MODULE_LICENSE("Dual BSD/GPL"),Linux可接受的 LICENSE 包括”GPL","GPL v2","GPL and additional rights","Dual BSD/GPL","Dual MPL/GPL","Proprietary"。可以不加,则系统默认。如果不声明 LICENSE ,模块被加载时,将收到内核的警告。
模块参数是“模块被加载的时候可以被传递给模块的值”,它本身对应模块内部的全部变量。 可以使用module_param(参数名,参数类型,读/写权限)为模块定义一个参数。
内核模块可以导出符号(symbol,对应与函数或变量),这样其他模块可以使用本模块中的变量和函数。/proc/kallsyms文件对应这内核符号表,它记录了符号以及符号符号所在的内存地址。
模块可以使用如下宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符合名); //只是用于GPL许可权模块。
导出的符合将可以被其他模块使用,使用前声明以下既可以。
模块作者的等信息声明:
MODULE_AUTOR("作者信息");
MODULE_DESCRIPTION("模块描述信息");
MODULE_VERSION("版本信息");
MODULE_ALIAS("别名信息");
MODULE_DEVICE_TABLE("设备表信息");
对于USB,PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,表示驱动所支持的设备列表。
三、编写内核模块的基本步骤
1、根据自己的需求编写内核模块源代码
2、将源代码进行编译,生成.ko文件
在编译内核模块时需要用到Makefile,
obj-m :=*.o
PWD := $(shell pwd)
KDIR:=/usr/src/linux-headers-4.4.0-21-generic
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
obj-m:这个变量是指定要编译的模块
KDIR:这是我们正在运行的操作系统内核编译目录,也就是编译模块需要的环境
PWD:这是当前工作路径,$(shell )是make的一个内置函数,用来执行shell命令
注意:要将Makefile文件与四个内核模块源代码放在同一个文件夹中。
3、用insmod命令加载模块
4、测试内核模块功能
5、用rmmod命令卸载模块
四、内核模块编程
proc模块
代码
proc.c代码:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h> // for basic filesystem
#include <linux/proc_fs.h> // for the proc filesystem
#include <linux/seq_file.h> // for sequence files
#include <linux/jiffies.h> // for jiffies
#include <linux/slab.h> // for kzalloc, kfree
#include <linux/uaccess.h> // for copy_from_user
//static struct task_struct *pcurrent;
int print_current_task_info(void);
// global var
static char *str = NULL;
// seq_operations -> show
static int jif_show(struct seq_file *m, void *v)
{
//seq_printf(m, "current kernel time is %llu\n", (unsigned long long) get_jiffies_64());
seq_printf(m, "str is %s\n", str);
return 0; //!! must be 0, or will show nothing T.T
}
// file_operations -> write
static ssize_t jif_write(struct file *file, const char __user *buffer, size_t count, loff_t *f_pos)
{
char *tmp = kzalloc((count+1), GFP_KERNEL);
if (!tmp)
return -ENOMEM;
//copy_to|from_user(to,from,cnt)
if (copy_from_user(tmp, buffer, count)) {
kfree(tmp);
return -EFAULT;
}
kfree(str);
str = tmp;
return count;
}
// seq_operations -> open
static int jif_open(struct inode *inode, struct file *file)
{
return single_open(file, jif_show, NULL);
}
static const struct file_operations jif_fops =
{
.owner = THIS_MODULE,
.open = jif_open,
.read = seq_read,
.write = jif_write,
.llseek = seq_lseek,
.release = single_release,
};
// module init
static int __init jif_init(void)
{
struct proc_dir_entry* jif_file;
jif_file = proc_create("exp2", 0, NULL, &jif_fops);
if (NULL == jif_file)
{
return -ENOMEM;
}
return 0;
}
// module exit
static void __exit jif_exit(void)
{
printk("******************************************\n");
remove_proc_entry("exp2", NULL);
kfree(str);
}
module_init(jif_init);
module_exit(jif_exit);
MODULE_AUTHOR("why");
MODULE_LICENSE("GPL");
测试过程及结果:
1. make
2. sudo insmod proc.ko
若要查看模块是否插入成功可以使用"lsmod | grep proc"查看
3. sudo -i
cat /proc/exp2
切换到root用户下,打印/proc/exp2文件中的信息,如图所示:
4. echo 5312 > /proc/exp2
输入"5312"到/proc/exp2文件中,若输入的信息有空格则需要在信息两侧加双引号
5. cat /proc/exp2
打印/proc/exp2中信息验证模块编程是否成功
syscall模块(系统调用)
代码
syscall代码:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/unistd.h>
#include <linux/sched.h>
MODULE_LICENSE("GPL");
#define SYS_CALL_TABLE_ADDRESS 0xc17ab180
#define NUM 23
int orig_cr0; *sys_call_table_my = 0;
static int (*anything_saved)(void);
static int clear_cr0(void)
{
unsigned int cr0 = 0;
unsigned int ret;
asm volatile ("movl %%cr0, %%eax":"=a"(cr0));
ret = cr0;
cr0 &= 0xfffeffff;
asm volatile ("movl %%eax, %%cr0": :"a"(cr0));
return ret;
}
static void setback_cr0(int val)
{
asm volatile ("movl %%eax, %%cr0": : "a"(val));
}
asmlinkage long sys_mycall(void)
{
printk("pid:%d, comm:%s\n", current->pid, current->comm);
return current->pid;
}
static int __init call_init(void)
{
sys_call_table_my = (unsigned long*)(SYS_CALL_TABLE_ADDRESS);
printk("call_init.......\n");
anything_saved = (int (*)(void))(sys_call_table_my[NUM]);
orig_cr0 = clear_cr0();
sys_call_table_my[NUM] =(unsigned long) &sys_mycall;
setback_cr0(orig_cr0);
return 0;
}
static void __exit call_exit(void)
{
printk("call_exit..........\n");
orig_cr0 = clear_cr0();
sys_call_table_my[NUM] = (unsigned long)anything_saved;
setback_cr0(orig_cr0);
}
module_init(call_init);
module_exit(call_exit);
MODULE_AUTHOR("Why");
MODULE_VERSION("v1.0");
MODULE_DESCRIPTION("A module for replace a syscall");
syscalltest代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main()
{
unsigned long x=0;
x=syscall(23);
printf("hello!why!%ld\n",x);
return 0;
}
测试过程及结果:
1. sudo cat /proc/kallsyms | grep sys_call_table
通过得到的本机系统调用表地址修改代码
2. make
gcc syscalltest.c -o syscalltest
3. sudo insmod syscall.ko
4. ./syscalltest
page模块(内存页表)
相关知识
当今,Linux采用了一种同时适用于32位和64位系统的普通分页模型。前面我们看到,两级页表对32位系统来说已经足够了,但64位系统需要更多数量的分页级别。直到2.6.10版本,Linux采用三级分页的模型。从2.6.11版本开始,采用了四级分页模型。
四级分页模型中的4种页表分别被称作:
• 页全局目录(Page Global Directory)
• 页上级目录(Page Upper Directory)
• 页中间目录(Page Middle Directory)
• 页表(Page Table)
页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址。每一个页表项指向一个页框。线性地址因此被分成五个部分。图中没有显示位数,因为每一部分的大小与具体的计算机体系结构有关。
对于没有启用物理地址扩展的32位系统,两级页表已经足够了。从本质上说Linux通过使“页上级目录”位和“页中间目录”位全为0,彻底取消了页上级目录和页中间目录字段。 不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个合适的目录项而实现的。
启用了物理地址扩展的32 位系统使用了三级页表。Linux的页全局目录对应80x86 的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。最终,64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。
pte_t、pmd_t、pud_t和 pgd_t分别描述页表项、页中间目录项、页上级目录和页全局目录项的类型格式。当PAE被激活时它们都是64位的数据类型,否则都是32位数据类型。 pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。
五个类型转换宏(__ pte、__ pmd、__ pud、__ pgd和__ pgprot)把一个无符号整数转换成所需的类型。另外的五个类型转换宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。
代码
page代码:
#include <linux/module.h>
#include <asm/pgtable.h>
#include <linux/version.h>
#include <asm/page.h>
#include <linux/gfp.h>
#include <linux/page-flags.h>
#include <linux/sched.h>//find_task_by_vpid
#include <linux/mm.h>//find_vma
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("CONVERT USER VIRTUAL ADDRESS TO PHYADDRESS");
static int pid;
static unsigned long va;
module_param(pid,int,0644);
module_param(va,ulong,0644);
static int find_pgd_init(void)
{
unsigned long pa=0;
struct task_struct *pcb_tmp=NULL;
pgd_t *pgd_tmp=NULL;
pud_t *pud_tmp=NULL;
pmd_t *pmd_tmp=NULL;
pte_t *pte_tmp=NULL;
printk(KERN_ALERT "test:va=0x%lx,pid=%d.\n",va,pid);
rcu_read_lock();
if( !( pcb_tmp = pid_task(find_vpid(pid), PIDTYPE_PID) ) )
{
rcu_read_unlock();
printk(KERN_ALERT "Can't find the task %d.\n",pid);
return 0;
}
rcu_read_unlock();
printk("The page index_table address = 0x%p\n\n",pcb_tmp->mm->pgd);
printk(KERN_ALERT "pgd=0x%p\n",pcb_tmp->mm->pgd);
if(!find_vma(pcb_tmp->mm,va))
{
printk(KERN_ALERT "virt_addr 0x%lx not available.\n",va);
return 0;
}
pgd_tmp=pgd_offset(pcb_tmp->mm,va);
printk(KERN_ALERT "pgd_tmp=0x%p\n",pgd_tmp);
printk(KERN_ALERT "pgd_val(*pgd_tmp)=0x%lx\n\n",pgd_val(*pgd_tmp));
if(pgd_none(*pgd_tmp))
{
printk(KERN_ALERT "Not mapped in pgd.\n");
return 0;
}
pud_tmp=pud_offset(pgd_tmp,va);
pmd_tmp=pmd_offset(pud_tmp,va);
pte_tmp=pte_offset_kernel(pmd_tmp,va);
if(pte_none(*pte_tmp))
{
printk(KERN_ALERT "Not mapped in pte.\n");
return 0;
}
if(!pte_present(*pte_tmp))
{
printk(KERN_ALERT "pte not in RAM,maybe swaped.\n");
return 0;
}
pa=(pte_val(*pte_tmp)&PAGE_MASK)|(va&~PAGE_MASK);
printk(KERN_ALERT "Virtual address: 0x%lx in RAM is 0x%lx.\n",va,pa);
printk(KERN_ALERT "Part content in 0x%lx is 0x%lx.\n",pa,*(unsigned long*)((char *)pa+PAGE_OFFSET));
int i;
printk("some content:\n");
for(i=0;i<40;i=i+4)
{
printk("%lx\n",*(unsigned long*)((char*)pa+PAGE_OFFSET+i));
}
return 0;
}
static void find_pgd_exit(void)
{
printk(KERN_ALERT "Goodbye.\n");
}
module_init(find_pgd_init);
module_exit(find_pgd_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Why");
MODULE_DESCRIPTION("GET WESSAGE");
测试过程及结果:
1. make
2. gedit page.c
ps -ef | grep gedit
objdump -d /usr/bin/gedit | more
使用一个我们想要查看的软件,查看pid,查看进程的入口地址。
使用计算机计算出地址的十进制值
3. sudo insmod page.ko pid=1111 va=134514700
4. dmesg | tail -n 20
linux实践——内核编程 基础模块的更多相关文章
- 20191310李烨龙Linux C语言编程基础
Linux C语言编程基础 任务详情 0. 基于Ubuntu或OpenEuler完成下面的任务(OpenEuler有加分) 1. 选择教材第二章的一节进行编程基础练习(2.10,2.11,2.12,2 ...
- linux下c编程 基础
1. 熟悉Linux系统下的开发环境 2. 熟悉vi的基本操作 3. 熟悉gcc编译器的基本原理 4. 熟练使用gcc编译器的常用选项 5 .熟练使用gdb调试技术 6. 熟悉makefile基本原理 ...
- Linux Shell脚本编程-基础1
概述: shell脚本在Linux系统管理员的运维工作中非常重要.shell脚本能够帮助我们很方便的管理服务器,因为我们可以指定一个任务计划,定时的去执行某一个脚本以满足我们的需求.本篇将从编程基础 ...
- Linux shell脚本编程基础之练习篇
shell脚本编程基础之练习篇. 1.编写一个脚本使我们在写一个脚本时自动生成”#!/bin/bash”这一行和注释信息. #!/bin/bash ] then echo "请输入一个参数& ...
- linux 实践2.2 编译模块
1. 理解模块原理 linux模块是一些可以作为独立程序来编译的函数和数据类型的集合.之所以提供模块机制,是因为Linux本身是一个单内核.单内核由于所有内容都集成在一起,效率很高,但可扩展性和可维 ...
- Linux Shell脚本编程基础(11)
实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核,不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序.Shel编程语言具有普通编程 ...
- (转)关于linux中内核编程中结构体的赋值操作(结构体指定初始化)
网址:http://blog.chinaunix.net/uid-24807808-id-3219820.html 在看linux源码的时候,经常会看到类似于下面的结构体赋值的代码: struct d ...
- windows内核编程基础知识
/* 1.基本的驱动数据结构 //驱动对象结构体 typedef struct _DRIVER_OBJECT { CSHORT Type; //结构类型 CSHORT Size; //结构大小 PDE ...
- Linux IPC socket编程基础
头文件 #include<unistd.h> #include <sys/types.h> #include <sys/socket.h> #include< ...
随机推荐
- eval 为什么加括号
<!DOCTYPE html> <html> <head> <title>eval学习</title> <script type=&q ...
- java中使用split分割字符串一个有趣的现象
最近在项目中,发现了一个bug,充分了展示了自己对java底层的认知有很多的不足和欠缺. 下面有段代码: String str="1#2#3"; String[] strs=str ...
- JS 中html 动态替换
一.定义通用替换js函数,或调用JQuery验证的$.format函数: //----通用JS操作// var a = "我喜欢吃{0},也喜欢吃{1},但是最喜欢的还是{0},偶尔再买点{ ...
- olcal数据库经典SQL语句大全
基于olacle自带的表 第一篇 -----1.列出至少有一个员工的所有部门. oracle 一些经典sql第一篇 --------1.列出至少有一个员工的所有部门.--------- SQL> ...
- 烂泥:为KVM虚拟机添加网卡
本文由秀依林枫提供友情赞助,首发于烂泥行天下. 前几篇文章介绍了有关KVM安装虚拟机以及如何给虚拟机添加硬盘,今天我们再来介绍下有关如何给KVM虚拟机添加网卡. 给KVM虚拟机添加网卡,可以分为两种形 ...
- poj 3177 Redundant Paths
题目链接:http://poj.org/problem?id=3177 边双连通问题,与点双连通还是有区别的!!! 题意是给你一个图(本来是连通的),问你需要加多少边,使任意两点间,都有两条边不重复的 ...
- android去掉顶部标题栏
在清单文件(manifest.xml)里面实现 <application> <activity android:name="cn.ui.activity.UserRegAc ...
- FileOutputFormat
TextOutputFormat<K,V> 默认输出字符串输出格式: SequenceFileOutputFormat<K,V> 序列化文件输出: MultipleOutput ...
- 边工作边刷题:70天一遍leetcode: day 101
dp/recursion的方式和是不是game无关,和game本身的规则有关:flip game不累加值,只需要一个boolean就可以.coin in a line II是从一个方向上选取,所以1d ...
- UESTC 882 冬马党 --状压DP
定义:dp[i][j]为状态为j时,第i行符合条件的状态数 转移方程:dp[i][j] += dp[i-1][t] //t为上一行状态,与当前行不冲突. 从第一行开始向下枚举,每次枚举当前行的状态 ...