雷达智富

首页 > 内容 > 程序笔记 > 正文

程序笔记

通过ioctl操作硬件端口的Linux内核模块代码

2024-07-02 39

前言

在Linux中,如果要对特定的硬件端口进行操作,用户空间是没有足够的权限的,可以在内核模块中实现端口的读写操作,然后用户空间中的程序通过内核模块的ioctl进行操作,相关的代码实现和操作记录备忘。

内核模块代码

以下是一个简单的Linux内核驱动,可以实现对特定IO端口进行读写操作。这个驱动可以将一个整数写入端口,并从端口读取一个整数,并将结果返回给用户空间。具体实现如下:

#include linux/init.h
#include linux/module.h
#include linux/kernel.h
#include linux/fs.h
#include asm/uaccess.h
#include linux/cdev.h
#include linux/ioctl.h
#include asm/io.h

#define DRIVER_NAME my_port_driver
#define IOCTL_WRITE_PORT _IOW(k, 1, int)
#define IOCTL_READ_PORT _IOR(k, 2, int)

MODULE_LICENSE(GPL);

static int my_port_driver_open(struct inode *inode, struct file *file);
static int my_port_driver_release(struct inode *inode, struct file *file);
static long my_port_driver_ioctl(struct file *file, unsigned int cmd, unsigned long arg);

static int port = 0x378;

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_port_driver_open,
    .release = my_port_driver_release,
    .unlocked_ioctl = my_port_driver_ioctl,
};

static dev_t dev;
static struct cdev my_cdev;

static int my_port_driver_init(void) {
    int ret = alloc_chrdev_region(dev, 0, 1, DRIVER_NAME);
    if (ret) {
        printk(KERN_ERR Failed to allocate char device region\n);
        return ret;
    }

    cdev_init(my_cdev, fops);
    my_cdev.owner = THIS_MODULE;

    ret = cdev_add(my_cdev, dev, 1);
    if (ret) {
        printk(KERN_ERR Failed to add char device\n);
        unregister_chrdev_region(dev, 1);
        return ret;
    }

    printk(KERN_INFO my_port_driver loaded\n);

    return 0;
}

static void my_port_driver_exit(void) {
    cdev_del(my_cdev);
    unregister_chrdev_region(dev, 1);
    printk(KERN_INFO my_port_driver unloaded\n);
}

static int my_port_driver_open(struct inode *inode, struct file *file) {
    return 0;
}

static int my_port_driver_release(struct inode *inode, struct file *file) {
    return 0;
}

static long my_port_driver_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    int tmp;

    switch (cmd) {
        case IOCTL_WRITE_PORT:
            if (copy_from_user(tmp, (int *)arg, sizeof(int))) {
                return -EFAULT;
            }

            outb(tmp, port);
            break;
        case IOCTL_READ_PORT:
            tmp = inb(port);

            if (copy_to_user((int *)arg, tmp, sizeof(int))) {
                return -EFAULT;
            }

            break;
        default:
            return -ENOTTY;
    }

    return 0;
}

module_init(my_port_driver_init);
module_exit(my_port_driver_exit);

这个驱动会注册一个字符设备,并且实现了open、release和ioctl三个函数。在用户空间,可以使用ioctl系统调用来调用驱动中的ioctl函数。这个驱动实现了两个命令:IOCTL_WRITE_PORT和IOCTL_READ_PORT。前者会将一个整数写入端口,后者会从端口读取一个整数。这个驱动代码中的inb和outb函数是内联汇编实现的,用于向端口发送和接收数据。inb函数会从指定端口读取一个字节,而outb函数会将一个字节写入指定端口。port变量表示要读写的IO端口的地址。需要注意的是,这个驱动没有进行足够的错误处理,例如没有检查用户空间指针是否合法。在实际的驱动开发中,需要进行更严格的错误处理。

如何使用

驱动编译时需要使用Linux内核源代码中提供的Makefile文件进行编译。具体编译步骤如下:

  1. 编写驱动代码并保存到文件中(例如my_port_driver.c)。
  2. 在Linux内核源代码目录中创建一个Kbuild文件,内容如下:
obj-m := my_port_driver.o
  1. 在Linux内核源代码目录中打开终端,执行以下命令进行编译:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

其中(unamer)表示当前内核版本,(uname -r)表示当前内核版本,(unamer)(pwd)表示当前目录。
4. 如果编译成功,会生成一个my_port_driver.ko文件。
加载驱动模块:

sudo insmod my_port_driver.ko

查看内核日志,确认驱动已经加载成功:

dmesg

可以使用ioctl系统调用进行读写操作。例如,向端口0x378写入数值100:

#include stdio.h
#include fcntl.h
#include sys/ioctl.h

#define DEVICE_NAME /dev/my_port_driver
#define IOCTL_WRITE_PORT _IOW(k, 1, int)

int main() {
    int fd = open(DEVICE_NAME, O_RDWR);

    if (fd  0) {
        perror(Failed to open device file);
        return -1;
    }

    int value = 100;
    if (ioctl(fd, IOCTL_WRITE_PORT, value)  0) {
        perror(Failed to write to port);
        return -1;
    }

    close(fd);

    return 0;
}

这个程序会向端口0x378写入数值100。

更新于:2个月前
赞一波!3

文章评论

全部评论