从零开始之驱动开发、linux驱动(一、驱动基础)

简介: linux驱动开发之驱动基础

在ARM工作模式中,处理器模式切换可以通过软件控制进行切换,即修改CPSR模式位,但这是在特权模式下,当我们处于用户模式下,是没有权限实现模式转换的。若想实现模式切换,只能由另一种方法来实现,即通过中断或是异常处理过程进行切换。于是ARM指令集中提供了两条产生异常的指令,通过这两条指令可以用软件的方法实现异常,其中一个就是中断指令swi(另一个是断点中断BKPT ).

软件中断指令(Software Interrupt, swi)用于产生软中断,实现从用户模式变换到管理模式,CPSR保存到管理模式的SPSR中,执行转移到swi向量。在其他模式下也可以使用SWI指令,处理器同样切换到管理模式。

1.SWI指令格式如下:
20180827210530671.png
20180827210614325.png

cond 是执行指令的条件

immed_24 24位立即数,值为从0――16777215之间的整数

SWI指令后面的24立即数是干什么用的呢?用户程序通过SWI指令切换到特权模式,进入软中断处理程序,但是软中断处理程序不知道用户程序到底想要做什么?SWI指令后面的24位用来做用户程序和软中断处理程序之间的接头暗号。通过该软中断立即数来区分用户不同操作,执行不同内核函数。如果用户程序调用系统调用时传递参数,根据ATPCSC语言与汇编混合编程规则将参数放入R0~R4即可。

使用SWI指令时,通常使用以下两种方法进行参数传递,SWI异常处理程序可以提供相关的服务,这两种方法均是用户软件协定。SWI异常中断处理程序要通过读取引起软件中断的SWI指令,以取得24为立即数。

1)、指令中24位的立即数指定了用户请求的服务类型,中断服务的参数通过通用寄存器传递。

如下面这个程序产生一个中断号位12 的软中断:

MOV R0,#34                    ;设置功能号为34
   
  SWI 12                              ;产生软中断,中断号为12

2)、指令中的24位立即数被忽略,用户请求的服务类型有寄存器R0的值决定,参数通过其他的通用寄存器传递。

如下面的例子通过R0传递中断号,R1传递中断的子功能号:

MOV R0, #12                  ;设置12号软中断
  
 MOV R1, #34                  ;设置功能号为34
  
 SWI  0

操作系统的主要功能是为应用程序的运行创建良好的环境,保障每个程序都可以最大化利用硬件资源,防止非法程序破坏其它应用程序执行环境,为了达到这个目的,操作系统会将硬件的操作权限交给内核程序来管理,用户程序不能随意使用硬件,使用硬件(对硬件寄存器进行读写)时要先向操作系统发出请求,操作系统内核帮助用户程序实现其操作,也就是说用户程序不会直接操作硬件,而是提供给用户程序一些具备预定功能的内核函数,通过一组称为系统调用的(system call)的接口呈现给用户,系统调用把应用程序的请求传给内核,调用相应的内核函数完成所需的处理,将处理结果返回给应用程序。

操作系统里将用户程序运行在用户模式下,并且为其分配可以使用内存空间,其它内存空间不能访问,内核态运行在特权模式下,对系统所有硬件进行统一管理和控制。从前面所学知识可以了解到,用户模式下没有权限进行模式切换,这也就意味着用户程序不可能直接通过切换模式去访问硬件寄存器,如果用户程序试图访问没有权限的硬件,会产生异常。这样用户程序被限制起来,如果用户程序想要使用硬件时怎么办呢?用户程序使用硬件时,必须调用操作系统提供的API接口才可以,而操作系统API接口通过软件中断方式切换到管理模式下,实现从用户模式下进入特权模式。

在3.16.57的内核中总共有382个系统调用

arch/arm/kernel/colls.S

    /* 0 */        CALL(sys_restart_syscall)
            CALL(sys_exit)
            CALL(sys_fork)
            CALL(sys_read)
            CALL(sys_write)
    /* 5 */        CALL(sys_open)
            CALL(sys_close)
            CALL(sys_ni_syscall)        /* was sys_waitpid */
            CALL(sys_creat)
            CALL(sys_link)
     
            ......
     
    /* 375 */    CALL(sys_setns)
            CALL(sys_process_vm_readv)
            CALL(sys_process_vm_writev)
            CALL(sys_kcmp)
            CALL(sys_finit_module)
    /* 380 */    CALL(sys_sched_setattr)
            CALL(sys_sched_getattr)
            CALL(sys_renameat2)

其中CALL的定义如下,可以看出是直接定义为代码段的某个地址了,方便数组下标索引,可以看到sys_open的偏移是5

#define CALL(x) .long x

 

EABI (Extended ABI)

    CONFIG_OABI_COMPAT    //表示老的系统调用接口
    CONFIG_AEABI          //新的系统调用接口

EABI ,说的是这样的一种新的系统调用方式

mov r7, #num

swi 0x0

原来的系统调用方式是这样,

swi (#num | 0x900000) (0x900000是个magic值)

也就是说原来的调用方式(Old ABI)是通过跟随在swi指令中的调用号来进行的,现在的是根据r7中的值。

现在看两个宏,一个是

CONFIG_OABI_COMPAT 意思是说和old ABI兼容

另一个是

CONFIG_AEABI 意思是说指定现在的方式为EABI

这两个宏可以同时配置,也可以都不配,也可以配置任何一种。

我说一下内核是怎么处理这一问题的。

我们知道,sys_call_table 在内核中是个跳转表,这个表中存储的是一系列的函数指针,这些指针就是系统调用函数的指针,如(sys_open).系统调用是根据一个调用号(通常就是表的索引)找到实际该调用内核哪个函数,然后运行该函数完成的。

首先,对于old ABI,内核给出的处理是给它建立一个单独的system calltable,叫sys_oabi_call_table,这样,兼容方式下就会有两个system call table, 以oldABI方式的系统调用会执行old_syscall_table表中的系统调用函数,EABI方式的系统调用会用sys_call_table中的函数指针。

配置无外乎以下4中

第一 两个宏都配置 行为就是上面说的那样

第二 只配置CONFIG_OABI_COMPAT , 那么以old ABI方式调用的会用sys_oabi_call_table,以EABI方式调用的 用sys_call_table,和1实质相同,只是情况1更加明确。

第三 只配置CONFIG_AEABI 系统中不存在 sys_oabi_call_table, 对old ABI方式调用不兼容。只能 以EABI方式调用,用sys_call_table

第四 两个都没有配置 系统默认会只允许old ABI方式,但是不存在old_syscall_table,最终会通过sys_call_table 完成函数调用

用户空间如何让产生系统调用,即触发SWI异常

就看定义编译内核是默认是OABI还是EABI,可以看到新的EABI是r7传参方式

#ifndef CONFIG_CPU_THUMBONLY
 #define ARM_OK(code...)    code        //正常都是这种
 #else
 #define ARM_OK(code...)
 #endif
  
  
     .align
  
 sigreturn_codes:
  
     /* ARM sigreturn syscall code snippet */
     arm_slot 0
 ARM_OK(    mov    r7, #(__NR_sigreturn - __NR_SYSCALL_BASE)    )
 ARM_OK(    swi    #(__NR_sigreturn)|(__NR_OABI_SYSCALL_BASE)    )
  
     /* Thumb sigreturn syscall code snippet */
     thumb_slot 0
     movs    r7, #(__NR_sigreturn - __NR_SYSCALL_BASE)
     swi    #0
  
     /* ARM sigreturn_rt syscall code snippet */
     arm_slot 1
 ARM_OK(    mov    r7, #(__NR_rt_sigreturn - __NR_SYSCALL_BASE)    )
 ARM_OK(    swi    #(__NR_rt_sigreturn)|(__NR_OABI_SYSCALL_BASE)    )
  
     /* Thumb sigreturn_rt syscall code snippet */
     thumb_slot 1
     movs    r7, #(__NR_rt_sigreturn - __NR_SYSCALL_BASE)
     swi    #0

通过上面的calls.S和下面两句可以知道,sys_call_table就是系统调用表的首地址。而#include "call.S"里面的内容在前面已经说明了,

就是以 .long sys_xxx 的函数的地址

.type    sys_call_table, #object
    ENTRY(sys_call_table)
    #include "calls.S"

方然也有老的调用接口,但对里面的系统调用函数,明显都是一样的

.type    sys_oabi_call_table, #object
    ENTRY(sys_oabi_call_table)
    #include "calls.S"

下面就是SWI异常处理函数的实现

/*=============================================================================
     * 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 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)

真正的系统调用则是用过下面这个函数实现的。

asmlinkage long sys_open(const char __user *filename,
                 int flags, umode_t mode);
  
  
 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
 {
     if (force_o_largefile())
         flags |= O_LARGEFILE;
  
     return do_sys_open(AT_FDCWD, filename, flags, mode);
 }
  
  
  
 SYSCALL_DEFINE3是一个宏,可以自己解析一下,实际下面的两个是一样的
 asmlinkage long sys_open(const char __user *filename,int flags, umode_t mode);
 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode);

0_1322832443IQlL.jpg

对照上图,可以看到应用程序使用系统调用接口,需要从用户级切换到内核级。

方法是:通过SWI指令和寄存器传入参数,通过输入的swi中断号,直接以查表方式找到对应的系统调用函数。

以open一个led灯为例:

可以看到调用顺序依次是应用程序 open -> swi软中断 -> 系统调用接口 -> sys_open ->

文件子系统VFS中的open -> 驱动程序open -> 硬件操作

其中应用程序空间的open需要通过swi指令来实现swi中断。

通过文件的属性(普通文件,设备文件),来不同的处理。

如果是设备文件,则继续通过属性查看是字符还是块设备文件,找到对应的驱动程序,最终操纵硬件。

相关文章
|
5天前
|
Linux 编译器 Android开发
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
在Linux环境下,本文指导如何交叉编译x265的so库以适应Android。首先,需安装cmake和下载android-ndk-r21e。接着,下载x265源码,修改crosscompile.cmake的编译器设置。配置x265源码,使用指定的NDK路径,并在配置界面修改相关选项。随后,修改编译规则,编译并安装x265,调整pc描述文件并更新PKG_CONFIG_PATH。最后,修改FFmpeg配置脚本启用x265支持,编译安装FFmpeg,将生成的so文件导入Android工程,调整gradle配置以确保顺利运行。
24 1
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
|
1月前
|
Linux API 调度
Linux系统驱动跟裸机驱动的区别
Linux系统驱动跟裸机驱动的区别
29 0
|
1月前
|
Linux C语言 SoC
嵌入式linux总线设备驱动模型分析
嵌入式linux总线设备驱动模型分析
32 1
|
1月前
|
存储 缓存 Linux
【Shell 命令集合 磁盘维护 】Linux 设置和查看硬盘驱动器参数 hdparm命令使用教程
【Shell 命令集合 磁盘维护 】Linux 设置和查看硬盘驱动器参数 hdparm命令使用教程
36 0
|
1月前
|
分布式计算 关系型数据库 MySQL
Sqoop【部署 01】CentOS Linux release 7.5 安装配置 sqoop-1.4.7 解决警告并验证(附Sqoop1+Sqoop2最新版安装包+MySQL驱动包资源)
【2月更文挑战第8天】Sqoop CentOS Linux release 7.5 安装配置 sqoop-1.4.7 解决警告并验证(附Sqoop1+Sqoop2最新版安装包+MySQL驱动包资源)
100 1
|
11天前
|
Linux Go
Linux命令Top 100驱动人生! 面试必备
探索Linux命令不再迷茫!本文分10部分详解20个基础命令,带你由浅入深掌握文件、目录管理和文本处理。 [1]: <https://cloud.tencent.com/developer/article/2396114> [2]: <https://pan.quark.cn/s/865a0bbd5720> [3]: <https://yv4kfv1n3j.feishu.cn/docx/MRyxdaqz8ow5RjxyL1ucrvOYnnH>
64 0
|
21天前
|
Linux API C语言
FFmpeg开发笔记(一)搭建Linux系统的开发环境
本文指导初学者如何在Linux上搭建FFmpeg开发环境。首先,由于FFmpeg依赖第三方库,可以免去编译源码的复杂过程,直接安装预编译的FFmpeg动态库。推荐网站<https://github.com/BtbN/FFmpeg-Builds/releases>提供适用于不同系统的FFmpeg包。但在安装前,需确保系统有不低于2.22版本的glibc库。详细步骤包括下载glibc-2.23源码,配置、编译和安装。接着,下载Linux版FFmpeg安装包,解压至/usr/local/ffmpeg,并设置环境变量。最后编写和编译简单的C或C++测试程序验证FFmpeg环境是否正确配置。
37 8
FFmpeg开发笔记(一)搭建Linux系统的开发环境
|
24天前
|
Linux
Linux驱动运行灯 Heartbeat
Linux驱动运行灯 Heartbeat
12 0
|
1月前
|
存储 缓存 Linux
探秘Linux块设备驱动程序:成为内核开发大师的第一步
探秘Linux块设备驱动程序:成为内核开发大师的第一步
93 0