2019-09-25 21:25:14 qq_45683435 阅读数 38
  • 《C语言/C++学习指南》Linux开发篇

    本篇面向Linux初级、但已掌握C/C++基本语法的学员,以CentOS为演示平台,介绍Linux下的C/C++开发技术。在内容编排上,先介绍Linux的安装、基本使用和配置,然后再开始介绍在Linux下的C/C++开发技术: gcc命令行、Makefile、gdb调试方法、动态库与静态库、pthread线程、进程、进程间通信、系统调用及杂项技术。(前四章免费)

    59609 人正在学习 去看看 邵发
  1. 概述

本文学习基于armv7和armv8体系的linux系统调用机制,linux内核版本为3.10.79。通过分析系统调用机制和源代码来展示系统调用过程。

  1. 什么是系统调用

linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用很相似,仅仅是系统调用由操作系统核心提供,执行于核心态。而普通的函数调用由函数库或用户自己提供。执行于用户态。

通常系统调用过程如下:

用户程序------>C库(即API):软中断 ----->system_call------->系统调用服务函数------->内核程序。比如,  用户程序打开一个文件------>C库open:中断----->system_call------->sys_open------>内核程序------>返回到用户程序。

http://hi.csdn.net/attachment/201112/2/0_1322832443IQlL.gif

注:图片来自网络

应用编程接口(API)与系统调用的不同在于,前者只是一个函数定义,说明了如何获得一个给定的服务,而后者是通过软件中断向内核发出的一个明确的请求。POSIX标准针对API,而不针对系统调用。Unix系统给程序员提供了很多API库函数。libc的标准c库所定义的一些API引用了封装例程(wrapper routine)(其唯一目的就是发布系统调用)。通常情况下,每个系统调用对应一个封装例程,而封装例程定义了应用程序使用的API。反之则不然,一个API没必要对应一个特定的系统调用。从编程者的观点看,API和系统调用之间的差别是没有关系的:唯一相关的事情就是函数名、参数类型及返回代码的含义。然而,从内核设计者的观点看,这种差别确实有关系,因为系统调用属于内核,而用户态的库函数不属于内核。

  1. 系统调用意义

用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。

系统调用在用户空间进程和硬件设备之间加入了一个中间层。该层主要作用有三个:
(1) 它为用户空间提供了一种统一的硬件的抽象接口。

比方当须要读些文件的时候,应用程序就能够不去管磁盘类型和介质,甚至不用去管文件所在的文件系统究竟是哪种类型。
(2)系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核能够基于权限和其它一些规则对须要进行的访问进行裁决。

举例来说,这样能够避免应用程序不对地使用硬件设备,窃取其它进程的资源,或做出其它什么危害系统的事情。
(3) 每一个进程都执行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这样的考虑。假设应用程序能够任意访问硬件而内核又对此一无所知的话,就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中。系统调用是用户空间访问内核的惟一手段。除异常和中断外,它们是内核惟一的合法入口。

 

  1. 系统调用的实现

应用编程接口(API)内部实现向内核切换的机制。首先,API为系统调用设置参数,其中一个参数是系统调用编号,其它参数为API接口参数。参数设置完成后,程序执行“系统调用”指令。这个指令会导致一个异常使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序,它与硬件体系结构紧密相关,异常处理程序会根据系统调用编号跳转到系统调用函数地址,执行系统调用函数。

    1. 系统调用号

系统调用号来标识系统调用函数,区分不同调用函数,系统调用号是唯一的,独一无二的。用户态向内核态切换方式都是一样的,所以必须通过这个系统调用号作为参数,使内核能够识别用户态想调用哪个系统调用函数。

      1. ArmV7

文件/arch/arm/kernel/calls.S中定义了系统调用表,文件\arch\arm\include\uapi\asm\unistd.h中定义了系统调用号,编号从0开始,arm 32位系统中一个表项占用4个字节,地址为基地址+编号*4(编号左移2位),另外定义了编号的上限,当编号不在范围内时,调用,直接返回错误号。

系统调用号:

\arch\arm\include\uapi\asm\unistd.h


#if defined(__thumb__) || defined(__ARM_EABI__)

#define __NR_SYSCALL_BASE 0

#else

#define __NR_SYSCALL_BASE __NR_OABI_SYSCALL_BASE

#endif


/*

 * This file contains the system call numbers.

 */


#define __NR_restart_syscall         (__NR_SYSCALL_BASE+  0)

#define __NR_exit                  (__NR_SYSCALL_BASE+  1)

#define __NR_fork                 (__NR_SYSCALL_BASE+  2)

#define __NR_read                 (__NR_SYSCALL_BASE+  3)

#define __NR_write                        (__NR_SYSCALL_BASE+  4)

#define __NR_open                        (__NR_SYSCALL_BASE+  5)

#define __NR_close                        (__NR_SYSCALL_BASE+  6)


/arch/arm/kernel/calls.S

/* 0 */     CALL(sys_restart_syscall)

        CALL(sys_exit)

        CALL(sys_fork_wrapper)

        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)

/* 10 */    CALL(sys_unlink)

        CALL(sys_execve_wrapper)

        CALL(sys_chdir)

        CALL(OBSOLETE(sys_time))    /* used by libc4 */

        CALL(sys_mknod)

 

      1. ArmV8

文件:arch\arm64\kernel\syc.c, arch\arm64\include\asm\ unistd.h,

arch\arm64\include\upai\asm\ unistd.h, include\asm-generic\ unistd.h,

include\upai\\asm-generic\ unistd.h

 

syc.c中:定义了sys_call_table,包含了几个头文件unistd.h,最终\upai\\asm-generic\ unistd.h添加了其它系统调用。

#define __SYSCALL(nr, sym)       [nr] = sym,

/*

 * The sys_call_table array must be 4K aligned to be accessible from

 * kernel/entry.S.

 */

注意这里数组初始化用法,第一次见。

void *sys_call_table[__NR_syscalls] __aligned(4096) = {

        [0 ... __NR_syscalls - 1] = sys_ni_syscall,

#include <asm/unistd.h>

};


include\upai\\asm-generic\ unistd.h中:

/* fs/read_write.c */

#define __NR3264_lseek 62

__SC_3264(__NR3264_lseek, sys_llseek, sys_lseek)

#define __NR_read 63

__SYSCALL(__NR_read, sys_read) //相当于[nr] = sym, [__NR_read] = sys_read

#define __NR_write 64

__SYSCALL(__NR_write, sys_write)

#define __NR_readv 65

__SC_COMP(__NR_readv, sys_readv, compat_sys_readv)

#define __NR_writev 66

__SC_COMP(__NR_writev, sys_writev, compat_sys_writev)

#define __NR_pread64 67

__SC_COMP(__NR_pread64, sys_pread64, compat_sys_pread64)

#define __NR_pwrite64 68

__SC_COMP(__NR_pwrite64, sys_pwrite64, compat_sys_pwrite64)

#define __NR_preadv 69

__SC_COMP(__NR_preadv, sys_preadv, compat_sys_preadv)

#define __NR_pwritev 70

__SC_COMP(__NR_pwritev, sys_pwritev, compat_sys_pwritev)

 

    1. 系统调用参数传递

API接口中有参数,如ssize_t write (int fd, const void * buf, size_t count); 参数是通过寄存器传递到内核的。

armv7:通过寄存器r1-r5传递函数参数, r0存放函数返回值,r7存放系统调用号

armv8 aarch64:通过寄存器x1-x6传递函数参数,x0存放函数返回值,x8存放系统调用号。

文件:glibc中\sysdeps\unix\sysv\linux\arm\sysdep.h, \sysdeps\unix\ sysdep.h。

后面系统调用举例章节会详细描述。

    1. 中断指令

用户态通过执行中断指令切换到内核态,

armv7:swi 0

armv8 aarch64:svc 0

文件:glibc中\sysdeps\unix\sysv\linux\arm\sysdep.h, \sysdeps\unix\ sysdep.h

后面系统调用举例章节会详细描述。

    1. 系统调用中断处理程序
      1. Armv7

文件:linux/arch/arm/kernel/ entry-common.S

分析主要代码,vector_swi为swi中断处理函数,swi指令后,cpu会自动跳转到中断向量进行处理。关于中断,后续中断学习中再解析。

  1. 中断向量入口函数
ENTRY(vector_swi)

//保存栈信息和状态寄存器

        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//这里^表示被备份的sp lr 是usr_mode的寄存器不是目前所在svc_mode的寄存器

 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

        zero_fp


//把系统调用表指针存在tbl中

        adr   tbl, sys_call_table           @ load syscall table pointer


#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 //把r7中的中断号存储到scno变量中

 USER(   ldreq        scno, [lr, #-4]           )


local_restart:

        ldr    r10, [tsk, #TI_FLAGS]           @ check for syscall tracing

        stmdb      sp!, {r4, r5}                      @ push fifth and sixth args //把参数所在寄存器r4,r5入栈,因为ATPCS,参数传递只能使用r0-r3,所以r4,r5通过栈传递


        cmp scno, #NR_syscalls         @ check upper syscall limit //检查是否超过了最大调用号

        adr   lr, BSYM(ret_fast_syscall)     @ return address //设置返回地址到lr

        ldrcc pc, [tbl, scno, lsl #2]                @ call sys_* routine // scno小于NR_syscalls执行此指令,跳转到系统调用函数地址,tbl+scno*4,系统调用表首地址+调用号*4,每个指针占4字节


   // scno大于等于NR_syscalls执行如下指令

        add  r1, sp, #S_OFF

2:     mov why, #0                             @ no longer a real syscall

        cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)

        eor   r0, scno, #__NR_SYSCALL_BASE     @ put OS number back

        bcs   arm_syscall    

        b      sys_ni_syscall                 @ not private func //系统调用号错误处理函数


ENDPROC(vector_swi)

 

      1. ArmV8

只解析64位运行模式。

文件arch\arm64\kernel\ entry.S:

中断向量表:

ENTRY(vectors)

        ventry      el1_sync_invalid             // Synchronous EL1t

        ventry      el1_irq_invalid                        // IRQ EL1t

        ventry      el1_fiq_invalid                        // FIQ EL1t

        ventry      el1_error_invalid            // Error EL1t


        ventry      el1_sync                  // Synchronous EL1h

        ventry      el1_irq                              // IRQ EL1h

        ventry      el1_fiq                              // FIQ EL1h

        ventry      el1_error_invalid            // Error EL1h


        ventry      el0_sync                  // Synchronous 64-bit EL0  //svc为同步异常,中断向量处理,来自el0

        ventry      el0_irq                              // IRQ 64-bit EL0

        ventry      el0_fiq                              // FIQ 64-bit EL0

        ventry      el0_error_invalid            // Error 64-bit EL0

…

END(vectors)

 

//同步异常处理函数,svc指令,data abort, instruct abort等都是同步异常,这里需要判断哪种异常,关于中断,详细分析看中断学习笔记。

el0_sync:

        kernel_entry 0 //保存寄存器,状态

        msr daifclr, #1                          //enable fiq

        mrs  x25, esr_el1                     // read the syndrome register

        lsr    x24, x25, #ESR_EL1_EC_SHIFT // exception class

        cmp x24, #ESR_EL1_EC_SVC64         // SVC in 64-bit state

        b.eq el0_svc


/*

 * SVC handler.

 */

        .align       6

el0_svc:

        adrp stbl, sys_call_table          // load syscall table pointer 系统调用表

        uxtw        scno, w8                   // syscall number in w8 系统调用号X8寄存器

        mov sc_nr, #__NR_syscalls

el0_svc_naked:                                         // compat entry point

        stp   x0, scno, [sp, #S_ORIG_X0] // save the original x0 and syscall number

        disable_step x16

        isb

        enable_dbg

        enable_irq


        get_thread_info tsk

        ldr    x16, [tsk, #TI_FLAGS]          // check for syscall hooks

        tst    x16, #_TIF_SYSCALL_WORK

        b.ne __sys_trace

        adr   lr, ret_fast_syscall           // return address 返回地址

        cmp     scno, sc_nr                     // check upper syscall limit

        b.hs ni_sys

        ldr    x16, [stbl, scno, lsl #3]   // address in the syscall table 计算系统调用函数地址,基地址+系统调用号*8, 64位是8字节存放指针长度

        br     x16                           // call sys_* routine //跳转到系统调用

ni_sys:

        mov x0, sp

        b      do_ni_syscall  //系统调用号错误处理函数

ENDPROC(el0_svc)

 

 

 

 

 

 

    1. 系统调用函数

Armv7系统函数调用表在calls.S中,以sys_开头,具体定义方式在SYSCALL_DEFINE进行定义,举例如下,通过SYSCALL_DEFINE3定义,这里不在介绍具体定义方式。Armv8参考章节5.1。

函数执行完返回执行结果。

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)

{

        struct fd f = fdget(fd);

        ssize_t ret = -EBADF;


        if (f.file) {

                 loff_t pos = file_pos_read(f.file);

                 ret = vfs_read(f.file, buf, count, &pos);

                 file_pos_write(f.file, pos);

                 fdput(f);

        }

        return ret;

}

 

 

    1. 系统调用返回

系统调用函数返回结果一般成功是0,失败返回一个负值,并且把错误码放到全局变量errno中。返回值放在r0寄存器中。

      1. Armv7

文件:linux/arch/arm/kernel/ entry-common.S

返回时执行ret_fast_syscall,

ret_fast_syscall:

 UNWIND(.fnstart )

 UNWIND(.cantunwind )

        disable_irq                               @ disable interrupts

        ldr    r1, [tsk, #TI_FLAGS]

        tst    r1, #_TIF_WORK_MASK

        bne  fast_work_pending

        asm_trace_hardirqs_on


        /* perform architecture specific actions before user return */

        arch_ret_to_user r1, lr

        ct_user_enter


        restore_user_regs fast = 1, offset = S_OFF  //回复各寄存器后返回到用户空间

 UNWIND(.fnend          )


        .macro     restore_user_regs, fast = 0, offset = 0

        ldr    r1, [sp, #\offset + S_PSR]       @ get calling cpsr

        ldr    lr, [sp, #\offset + S_PC]! @ get pc

        msr  spsr_cxsf, r1                    @ save in spsr_svc

#if defined(CONFIG_CPU_V6)

        strex r1, r2, [sp]                        @ clear the exclusive monitor

#elif defined(CONFIG_CPU_32v6K)

        clrex                                  @ clear the exclusive monitor

#endif

        .if     \fast

        ldmdb      sp, {r1 - lr}^                     @ get calling r1 - lr

        .else

        ldmdb      sp, {r0 - lr}^                     @ get calling r0 - lr

        .endif

        mov r0, r0                                 @ ARMv5T and earlier require a nop

                                                  @ after ldm {}^

        add  sp, sp, #S_FRAME_SIZE - S_PC

        movs        pc, lr                                 @ return & move spsr_svc into cpsr

        .endm

      1. Armv8

文件arch\arm64\kernel\ entry.S:系统调用返回时执行ret_fast_syscall。

ret_fast_syscall:

        disable_irq                               // disable interrupts

        ldr    x1, [tsk, #TI_FLAGS]

        and  x2, x1, #_TIF_WORK_MASK

        cbnz x2, fast_work_pending

        tbz   x1, #TIF_SINGLESTEP, fast_exit

        disable_dbg

        enable_step x2

fast_exit:

        kernel_exit 0, ret = 1



.macro     kernel_exit, el, ret = 0

        ldp   x21, x22, [sp, #S_PC]             // load ELR, SPSR // ELR为返回用户空间,执行的地址

        .if     \el == 0

        ldr    x23, [sp, #S_SP]             // load return stack pointer

#ifdef CONFIG_ARM64_ERRATUM_845719

        tbz   x22, #4, 1f

#ifdef CONFIG_PID_IN_CONTEXTIDR

        mrs         x29, contextidr_el1

        msr         contextidr_el1, x29

1:

#else

        msr         contextidr_el1, xzr

1:

#endif

#endif

        .endif

        .if     \ret

        ldr    x1, [sp, #S_X1]                       // preserve x0 (syscall return)

        add  sp, sp, S_X2

        .else

        pop  x0, x1

        .endif

        pop  x2, x3                               // load the rest of the registers

        pop  x4, x5

        pop  x6, x7

        pop  x8, x9

        msr  elr_el1, x21                      // set up the return data

        msr  spsr_el1, x22

        .if     \el == 0

        msr  sp_el0, x23

        .endif

        pop  x10, x11

        pop  x12, x13

        pop  x14, x15

        pop  x16, x17

        pop  x18, x19

        pop  x20, x21

        pop  x22, x23

        pop  x24, x25

        pop  x26, x27

        pop  x28, x29

        ldr    lr, [sp], #S_FRAME_SIZE - S_LR // load LR and restore SP //LR为在用户空间函数跳转时保存的地址。而ELR是异常发生切换时,保存的地址。

        eret                                    // return to kernel

        .endm

 

  1. 系统调用举例

write为例,分析调用过程。

    1. 用户空间
      1. Armv7

用户程序调用write();

ssize_t write (int fd, const void * buf, size_t count); 

文件:glibc中\sysdeps\unix\sysv\linux\arm\sysdep.h, \sysdeps\unix\ sysdep.h

\sysdeps\unix\sysv\linux\arm\sysdep.h,write.c

  1. Write->__write(__libc_write)-> SYSCALL_CANCEL

ssize_t

__libc_write (int fd, const void *buf, size_t nbytes)

{

  return SYSCALL_CANCEL (write, fd, buf, nbytes); //3个参数转化为4个,添加了函数名write

}

libc_hidden_def (__libc_write)

weak_alias (__libc_write, __write)

 

  1. SYSCALL_CANCEL-> INLINE_SYSCALL_CALL->__INLINE_SYSCALL4-> INLINE_SYSCALL

INLINE_SYSCALL_CALL宏定义会根据参数个数转换为_INLINE_SYSCALL4

#define SYSCALL_CANCEL(...) \

  ({                                                                         \

    long int sc_ret;                                                       \

    if (SINGLE_THREAD_P)                                                   \

      sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__);                       \

    else                                                                  \

      {                                                                          \

        int sc_cancel_oldtype = LIBC_CANCEL_ASYNC ();                       \

        sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__);                            \

        LIBC_CANCEL_RESET (sc_cancel_oldtype);                                  \

      }                                                                          \

    sc_ret;                                                                     \

  })

 

#define __INLINE_SYSCALL4(name, a1, a2, a3, a4) \

  INLINE_SYSCALL (name, 4, a1, a2, a3, a4)

 

#define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n

#define __INLINE_SYSCALL_NARGS(...) \

  __INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,)

#define __INLINE_SYSCALL_DISP(b,...) \

  __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__) //转换,如果有4个参数,那么__VA_ARGS__占用a,b,c  那n的位置正好是3,所以__INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n 计算后为(write, fd, buf, nbytes,7,6,5,4, 3,2,1,0,...)  4

 

#define INLINE_SYSCALL_CALL(...) \

  __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__)

 

 

  1. INLINE_SYSCALL-> INTERNAL_SYSCALL-> INTERNAL_SYSCALL_RAW

#define INLINE_SYSCALL(name, nr, args...)                               \

  ({ unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args);        \

 

#define INTERNAL_SYSCALL(name, err, nr, args...)         \

        INTERNAL_SYSCALL_RAW(SYS_ify(name), err, nr, args) // SYS_ify计算系统调用号

 

 

  1. # define INTERNAL_SYSCALL_RAW(name, err, nr, args...)             \

  ({                                                            \

       register int _a1 asm ("r0"), _nr asm ("r7");               \//定义变量_a1,_nr存放在寄存器r0r7中,

       LOAD_ARGS_##nr (args)                                        \  //加载参数args寄存器中,nr为个数

       _nr = name;                                                 \ //存放系统号

       asm volatile ("swi     0x0  @ syscall " #name  \//swi指令,切换到内核

                      : "=r" (_a1)                             \ //输出,返回值放在r0

                      : "r" (_nr) ASM_ARGS_##nr                        \ //变量参数

                      : "memory");                          \  //告诉编译器,这段内嵌式汇编代码,所有变量通过内存获取或存储,不使用缓存

       _a1; })

 

      1. Armv8

接口和调用过程一样,只是最后中断指令和使用寄存器不同,armv8如下,

 

# define INTERNAL_SYSCALL_RAW(name, err, nr, args...)             \

  ({ long _sys_result;                                              \

     {                                                               \

       LOAD_ARGS_##nr (args)                                        \//参数存储在x1-x5

       register long _x8 asm ("x8") = (name);                     \//x8存储系统调用号

       asm volatile ("svc     0      // syscall " # name     \//中断指令svc,切换到内核

                      : "=r" (_x0) : "r"(_x8) ASM_ARGS_##nr : "memory");     \

       _sys_result = _x0;                                      \

     }                                                               \

     _sys_result; })

 

    1. 内核空间

内核空间执行,前面已经分析了,

中断后,cpu跳转到中断向量处理函数vector_swi,之后查找系统调用表,跳转到asmlinkage long sys_write(unsigned int fd, const char __user *buf,

                           size_t count);

执行完,返回用户空间。

Armv8类似,前面章节已分析。

 

 

  1. 如何添加系统调用

系统调用设计原则保持函数通用性,简洁性,可移植性,健壮性。

    1. Armv7
      1. 定义系统调用函数,

放在相关功能的.c文件中,如文件相关的在fs/read_write.c等

SYSCALL_DEFINE3(write_example, unsigned int, fd, const char __user *, buf,

                 size_t, count)

{

……

…….

        return ret;

}

SYSCALL_DEFINE 使用与参数个数有关,n个参数就使用SYSCALL_DEFINEn,

SYSCALL_DEFINEn(xxx)中第一个参数为函数名,后面是函数参数内容,这里参数类型和变量用逗号隔开。

      1. 添加系统调用声明

在相关.h文件添加声明,通用函数一般在kernel\linux-3.10.y\include\linux\syscalls.h中添加声明,与体系相关的对应体系目录下syscalls.h文件中添加声明

asmlinkage long sys_ write_example (unsigned int fd, const char __user *buf,

                           size_t count);

 

      1. 添加系统调用号

在体系相关头文件unistd.h添加系统调用号

如armv7:\arch\arm\include\uapi\asm\unistd.h 最后添加系统调用号

#define __NR_finit_module           (__NR_SYSCALL_BASE+379)

#define __NR_seccomp                  (__NR_SYSCALL_BASE+383)

#define __NR_ write_example                        (__NR_SYSCALL_BASE+384)

 

      1. 添加系统调用表项

在体系相关文件中添加系统调用表

如armv7:/arch/arm/kernel/calls.S

/* 380 */  CALL(sys_ni_syscall)            /* reserved sys_sched_setattr */

                 CALL(sys_ni_syscall)            /* reserved sys_sched_getattr */

                 CALL(sys_ni_syscall)            /* reserved sys_renameat2     */

                 CALL(sys_seccomp)

CALL(sys_ write_example)

至此系统调用添加完成;

      1. 系统调用访问

一种方法是添加到glibc库中,作为一个库函数,添加方式参考章节6.另一种方式使用库函数syscall函数直接调用,如ret = syscall(系统调用号,参数….)

2019-09-12 00:40:25 oqqYuJi12345678 阅读数 37
  • 《C语言/C++学习指南》Linux开发篇

    本篇面向Linux初级、但已掌握C/C++基本语法的学员,以CentOS为演示平台,介绍Linux下的C/C++开发技术。在内容编排上,先介绍Linux的安装、基本使用和配置,然后再开始介绍在Linux下的C/C++开发技术: gcc命令行、Makefile、gdb调试方法、动态库与静态库、pthread线程、进程、进程间通信、系统调用及杂项技术。(前四章免费)

    59609 人正在学习 去看看 邵发

本文基于linux3.10内核

1 应用层处理

在Linux下系统调用是用软中断实现的,下面以一个简单的open例子简要分析一下应用层的open是如何调用到内核中的sys_open的。在glibc库中,通过封装例程(Wrapper Routine)将API和系统调用关联起来。API是头文件中所定义的函数接口,而位于glibc中的封装例程则是对该API对应功能的具体实现。事实上,我们知道接口open()所要完成的功能是通过系统调用open()完成的,因此封装例程要做的工作就是先将接口open()中的参数复制到相应的寄存器中,然后引发一个异常,从而系统进入内核区执行sys_open(),最后当系统调用执行完毕后,封装例程还要将错误码返回到应用程序中。

先来看一下下面这段程序:

int main(){
        int fd;
        fd=open(".",O_RDWR);
}

然后对该程序进行静态编译,并且反汇编:

arm-linux-gcc -o test main.c --static
arm-linux-objdump -D test
000082f0 <main>:
    82f0:	e92d4800 	push	{fp, lr}
    82f4:	e28db004 	add	fp, sp, #4	; 0x4
    82f8:	e24dd010 	sub	sp, sp, #16	; 0x10
    82fc:	ebffffd5 	bl	8258 <test>
    8300:	e1a03000 	mov	r3, r0
    8304:	e50b300c 	str	r3, [fp, #-12]
    8308:	e59f002c 	ldr	r0, [pc, #44]	; 833c <main+0x4c>
    830c:	e3a01002 	mov	r1, #2	; 0x2
    8310:	eb002e82 	bl	13d20 <__libc_open>
    8314:	e1a03000 	mov	r3, r0
    8318:	e50b3008 	str	r3, [fp, #-8]
    831c:	e59f301c 	ldr	r3, [pc, #28]	; 8340 <main+0x50>

可以看到在main 函数中,调用了__libc_open函数,再看一下__libc_open:

00013d20 <__libc_open>:
   13d20:	e51fc028 	ldr	ip, [pc, #-40]	; 13d00 <___fxstat64+0x50>
   13d24:	e79fc00c 	ldr	ip, [pc, ip]
   13d28:	e33c0000 	teq	ip, #0	; 0x0
   13d2c:	1a000006 	bne	13d4c <__libc_open+0x2c>
   13d30:	e1a0c007 	mov	ip, r7
   13d34:	e3a07005 	mov	r7, #5	; 0x5    //open的系统调用号5
   13d38:	ef000000 	svc	0x00000000   //产生软中断
   13d3c:	e1a0700c 	mov	r7, ip
   13d40:	e3700a01 	cmn	r0, #4096	; 0x1000
   13d44:	312fff1e 	bxcc	lr
   13d48:	ea0008b0 	b	16010 <__syscall_error>

__libc_open的实现应该在glib封装函数中,以r7作为传递系统调用号的寄存器,并且使用svc    0x00000000命令陷入内核,这种处理方式说明使用的是比较新的EABI方式的系统调用。

注:好多资料上说陷入软中断是使用SWI指令,ARM官网解释:

SVC 超级用户调用。
语法:

SVC{cond} #immed

其中:

cond

    是一个可选的条件代码(请参阅条件执行)。
immed

    是一个表达式,其取值为以下范围内的一个整数:

        在 ARM 指令中为 0 到 224–1(24 位值)

        在 16 位 Thumb 指令中为 0-255(8 位值)。

用法

SVC 指令会引发一个异常。 这意味着处理器模式会更改为超级用户模式,CPSR 会保存到超级用户模式 SPSR,并且执行会跳转到 SVC 向量(请参阅《开发指南》中的第 6 章 处理处理器异常)。

处理器会忽略 immed。 但异常处理程序会获取它,借以确定所请求的服务。
Note

作为 ARM 汇编语言开发成果的一部分,SWI 指令已重命名为 SVC。 在此版本的 RVCT 中,SWI 指令反汇编为 SVC,并提供注释以指明这是以前的 SWI。
条件标记

此指令不更改标记。
体系结构

此 ARM 指令可用于所有版本的 ARM 体系结构。

 中断向量表的初始化参考如下博文:

https://blog.csdn.net/oqqYuJi12345678/article/details/99654760

上面从反汇编来推测libc对于linux 系统调用的封装方式,下面摘录一下网上其他网友分析的libc源码的具体实现形式,具体情况估计是由于libc版本的原因跟反汇编看到的有些差异,不过基本思想是一致的!

通常情况下,我们写的用户空间应用程序都是通过封装的C lib来调用系统调用的。以0.9.30版uClibc中的open为例,来追踪一下这个封装的函数是如何一步一步的调用系统调用的。在include/fcntl.h中有定义:

# define open open64

open实际上只是open64的一个别名而已。

在libc/sysdeps/linux/common/open64.c中可以看到:

extern __typeof(open64) __libc_open64;

extern __typeof(open) __libc_open;

可见open64也只不过是__libc_open64的别名,而__libc_open64函数在同一个文件中定义:

libc_hidden_proto(__libc_open64)
int __libc_open64 (const char *file,int oflag, ...)
{
   mode_t mode = 0;
   if (oflag & O_CREAT)
   {
   va_listarg;
   va_start(arg, oflag);
   mode= va_arg (arg, mode_t);
   va_end(arg);
   }
   return __libc_open(file, oflag | O_LARGEFILE, mode);
}
libc_hidden_def(__libc_open64)

最终__libc_open64又调用了__libc_open函数,这个函数在文件libc/sysdeps/linux/common/open.c中定义:

libc_hidden_proto(__libc_open)
int __libc_open(const char *file, intoflag, ...)
{
   mode_tmode = 0;
   if(oflag & O_CREAT) {
      va_listarg;
      va_start(arg, oflag);
      mode= va_arg (arg, mode_t);
      va_end (arg);
   }
   return__syscall_open(file, oflag, mode);
}
libc_hidden_def(__libc_open)

这个函数,也是仅仅根据打开标志oflag的值,来判断是否有第三个参数,若由,则获得其值。之后,便用获得的参数来调用__syscall_open(file,oflag, mode)

__syscall_open在同一个文件中定义:

static __inline__ _syscall3(int,__syscall_open, const char *, file,
      int,flags, __kernel_mode_t, mode)

在文件libc/sysdeps/linux/arm/bits/syscalls.h文件中可以看到:

#undef _syscall3
#define_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3arg3) \
{ \
    return (type) (INLINE_SYSCALL(name, 3,arg1, arg2, arg3)); \
}

这个宏实际上完成定义一个函数的工作,宏的第一个参数是函数的返回值类型,第二个参数是函数名,之后的参数就如同它们的参数名所表明的那样,分别是函数的参数类型及参数名。__syscall_open实际上为:
 

int __syscall_open (const char * file,intflags, __kernel_mode_t mode)
{
    return (int) (INLINE_SYSCALL(__syscall_open,3, file, flags, mode));
}

INLINE_SYSCALL为同一个文件中定义的宏:

#undef INLINE_SYSCALL
#define INLINE_SYSCALL(name, nr,args...)            \
 ({ unsigned int _inline_sys_result = INTERNAL_SYSCALL (name, , nr,args);   \
    if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_inline_sys_result, ),0))  \
      {                        \
        __set_errno (INTERNAL_SYSCALL_ERRNO(_inline_sys_result, ));    \
        _inline_sys_result = (unsigned int) -1;          \
      }                        \
    (int) _inline_sys_result; })

INLINE_SYSCALL宏中最值得注意的是INTERNAL_SYSCALL,其定义如下:

#undef INTERNAL_SYSCALL
#if !defined(__thumb__)
#if defined(__ARM_EABI__)
#define INTERNAL_SYSCALL(name, err, nr,args...)        \
 ({unsigned int __sys_result;                 \
    {                          \
      register int _a1 __asm__ ("r0"), _nr __asm__ ("r7");    \
      LOAD_ARGS_##nr (args)                \
      _nr = SYS_ify(name);                 \
      __asm__ __volatile__ ("swi  0x0   @ syscall " #name  \
              : "=r" (_a1)            \
              : "r" (_nr) ASM_ARGS_##nr        \
              : "memory");            \
          __sys_result = _a1;               \
    }                          \
    (int) __sys_result; })
#else /* defined(__ARM_EABI__) */
#define INTERNAL_SYSCALL(name, err, nr,args...)        \
 ({ unsigned int __sys_result;                \
    {                          \
      register int _a1 __asm__ ("a1");               \
      LOAD_ARGS_##nr (args)                \
      __asm__ __volatile__ ("swi  %1 @ syscall " #name  \
           : "=r" (_a1)               \
           : "i" (SYS_ify(name))ASM_ARGS_##nr    \
           : "memory");               \
      __sys_result = _a1;                  \
    }                          \
    (int) __sys_result; })
#endif

这里也将同文件中的LOAD_ARGS宏的定义贴出来:

#define LOAD_ARGS_0()
#define ASM_ARGS_0
#define LOAD_ARGS_1(a1)           \
 _a1 = (int) (a1);            \
 LOAD_ARGS_0 ()
#define ASM_ARGS_1 ASM_ARGS_0, "r" (_a1)
#define LOAD_ARGS_2(a1, a2)       \
 register int _a2 __asm__ ("a2") = (int) (a2);   \
 LOAD_ARGS_1 (a1)
#define ASM_ARGS_2 ASM_ARGS_1, "r" (_a2)
#define LOAD_ARGS_3(a1, a2, a3)         \
 register int _a3 __asm__ ("a3") = (int) (a3);   \
 LOAD_ARGS_2 (a1, a2)

这几个宏用来在寄存器中加载相应的参数,参数传递的方式和普通的C函数也没有什么太大的区别,同样都是将参数列表中的参数依次放入寄存器r0、r1、r2、r3…中。

上面的SYS_ify(name)宏,是用来获得系统调用号的。

#define SYS_ify(syscall_name)  (__NR_##syscall_name)

也就是__NR___syscall_open,在libc/sysdeps/linux/common/open.c中可以看到这个宏的定义:

#define __NR___syscall_open __NR_open

__NR_open在内核代码的头文件中有定义。

在这里我们忽略定义__thumb__的情况,而假设我们编译出来的库函数使用的都是ARM指令集。在上面的代码中,我们看到,根据是否定义宏__ARM_EABI__INTERNAL_SYSCALL会被展开为两种不同的版本。关于这一点,与应用二进制接口ABI有关,不同的ABI,则会有不同的传递系统调用号的方法。对于比较新的EABI,则在r7寄存器保存系统调用号,通过swi   0x0来陷入内核。否则,通过swi指令的24位立即数参数来传递系统调用号。后面还会有内核中关于这个问题的更详细的说明。

同时这两种调用方式的系统调用号也是存在这区别的,在内核的文件arch/arm/inclue/asm/unistd.h中可以看到:

#define __NR_OABI_SYSCALL_BASE 0x900000
#if defined(__thumb__) ||defined(__ARM_EABI__)
    #define __NR_SYSCALL_BASE   0
#else
    #define __NR_SYSCALL_BASE   __NR_OABI_SYSCALL_BASE
#endif


/*
 * This file contains the system call numbers.
 */

#define __NR_restart_syscall      (__NR_SYSCALL_BASE+  0)
#define __NR_exit        (__NR_SYSCALL_BASE+  1)
#define __NR_fork        (__NR_SYSCALL_BASE+  2)
#define __NR_read        (__NR_SYSCALL_BASE+  3)
#define __NR_write       (__NR_SYSCALL_BASE+  4)
#define __NR_open        (__NR_SYSCALL_BASE+  5)

最终INLINE_SYSCALL展开如下:

#if defined(__ARM_EABI__)
#define INTERNAL_SYSCALL(name, err, nr,args...)        \
 ({unsigned int __sys_result;                 \
    {                          \
      register int _a1 __asm__ ("r0"), _nr __asm__ ("r7");    \
      register int _a3 __asm__ ("a3") = (int) (a3); \
      register int _a2 __asm__ ("a2") = (int) (a2);\
      _nr = __NR_open;             \    \
      __asm__ __volatile__ ("swi  0x0   @ syscall " #name  \
              : "=r" (_a1)            \
              : "r" (_nr) ,"r" (_a1),"r" (_a2),"r" (_a3)      \
              : "memory");            \
          __sys_result = _a1;               \
    }                          \
    (int) __sys_result; })
#else /* defined(__ARM_EABI__) */
#define INTERNAL_SYSCALL(name, err, nr,args...)        \
 ({ unsigned int __sys_result;                \
    {                          \
      register int _a1 __asm__ ("a1");               \
      register int _a3 __asm__ ("a3") = (int) (a3);
      register int _a2 __asm__ ("a2") = (int) (a2);              \
      __asm__ __volatile__ ("swi  %1 @ syscall " #name  \
           : "=r" (_a1)               \
           : "i" (__NR_open),"r" (_a1),"r" (_a2),"r" (_a3)    \
           : "memory");               \
      __sys_result = _a1;                  \
    }                          \
    (int) __sys_result; })
#endif

上面代码参考自该博文:https://blog.csdn.net/hongjiujing/article/details/6831192

2 内核层处理

linux异常处理过程如下图所示:

2.1 系统调用陷入

linux内核的中断向量表如下所示:

__vectors_start:
 ARM(	swi	SYS_ERROR0	)
 THUMB(	svc	#0		)
 THUMB(	nop			)
	W(b)	vector_und + stubs_offset
	W(ldr)	pc, .LCvswi + stubs_offset  //系统调用的中断向量
	W(b)	vector_pabt + stubs_offset
	W(b)	vector_dabt + stubs_offset
	W(b)	vector_addrexcptn + stubs_offset
	W(b)	vector_irq + stubs_offset
	W(b)	vector_fiq + stubs_offset

当使用svc 指令陷入内核以后,执行的中断向量函数就是这条:W(ldr)    pc, .LCvswi + stubs_offset

具体看一下vector_swi的实现,源码在arch/arm/kernel/entry-common.S中

ENTRY(vector_swi)
	sub	sp, sp, #S_FRAME_SIZE   //陷入内核以后,sp已经切换成svc特权模式下的栈了,先把该栈顶向下移动S_FRAME_SIZE个单位,为保留操作做准备
	stmia	sp, {r0 - r12}			@ Calling r0 - r12  //把r0 ~ r12寄存器放入栈中,ia是increase after的意思,r0放在栈顶的位置,不更新sp
 ARM(	add	r8, sp, #S_PC		)  //移动r8到sp+#S_PC的位置
 ARM(	stmdb	r8, {sp, lr}^		)	@ Calling sp, lr  //指令中的“^”符号表示访问user mode的寄存器,db”是decrement before,该指令把用户态的sp,和lr放到栈中偏移#S_PC+4的位置
	mrs	r8, spsr		//读取用户态的cpsr	@ called from non-FIQ mode, so ok.
	str	lr, [sp, #S_PC]		//把返回地址放入栈中偏移#S_PC的位置	@ Save calling PC
	str	r8, [sp, #S_PSR]	//保存	用户态的cpsr@ Save CPSR
	str	r0, [sp, #S_OLD_R0]	//保存旧的r0,后面会用到该寄存器	@ Save OLD_R0
	zero_fp
------------------------------------------(1)
	/*
	 * 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.
	 */
//当定义了老的OABI传参模式,则从返回地址-4,得到原来那条svc 陷入指令
	ldr	r10, [lr, #-4]			@ get SWI instruction
#ifdef CONFIG_CPU_ENDIAN_BE8
	rev	r10, r10			@ little endian instruction
#endif

#elif defined(CONFIG_AEABI)
//如果定义了AEABI,则系统调用号在r7中,这边什么都不做
	/*
	 * Pure EABI user space always put syscall number into scno (r7).
	 */

	/* Legacy ABI only. */
#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
	ldreq	scno, [lr, #-4]

#else
	/* Legacy ABI only. */
	ldr	scno, [lr, #-4]			@ get SWI instruction
#endif
#ifdef CONFIG_ALIGNMENT_TRAP
	ldr	ip, __cr_alignment
	ldr	ip, [ip]
	mcr	p15, 0, ip, c1, c0		@ update control register
#endif
	enable_irq  //陷入异常以后cpu 默认禁止中断,这边开启中断
	ct_user_exit

	get_thread_info tsk  //tsk是r9 寄存器的别名,把thread_info放入r9寄存器
-----------------------------------(2)
	adr	tbl, sys_call_table		@ load syscall table pointer//tbl是r8寄存器的别名,把系统调用表放入r8寄存器中
------------------------------------(3)

#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  //OABI 方式从svc指令中取出系统调用号,放入scno中
	ldrne	tbl, =sys_oabi_call_table //OABI用的系统调用表更新为sys_oabi_call_table,其实起始地址和sys_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]    //取出thread_info中的TI_FLAGS放入r10中,该位跟是否需要调度有关@ check for syscall tracing
	stmdb	sp!, {r4, r5}		//把r4,r5压入栈,!符号表示同时更新sp的值,这个时候的sp的值是刚陷入内核的时候的sp-#S_FRAME_SIZE-8	@ push fifth and sixth args
-----------------------------------------------------(4)

	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  //利用系统调用表加系统调用号的偏移,跳转到对应的系统调用函数进行处理
      -------------------------------------------(5)
	add	r1, sp, #S_OFF
2:	mov	why, #0				@ no longer a real syscall
	cmp	scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
	eor	r0, scno, #__NR_SYSCALL_BASE	@ put OS number back
	bcs	arm_syscall	
	b	sys_ni_syscall			@ not private func
ENDPROC(vector_swi)

内核定义了如下寄存器别名:

scno    .req    r7        @ syscall number
tbl    .req    r8        @ syscall table pointer
why    .req    r8        @ Linux syscall (!= 0)
tsk    .req    r9        @ current thread_info

(1)主要是保留现场做的处理,上面处理完以后,sp的形式如下:

高地址
---------------------------------------------------陷入内核时sp的位置
S_OLD_R0,	
S_PSR,	
S_PC,	
S_LR,	
S_SP,	
S_IP,	
S_FP,	
S_R10,	
S_R9,	
S_R8,	
S_R7,	
S_R6,	
S_R5,	
S_R4,	
S_R3,	
S_R2
S_R1
S_R0	
----------------------------------------------------sp保存完寄存器的地址
低地址

(2)get_thread_info tsk

get_thread_info是个宏定义:

.macro	get_thread_info, rd
mov	\rd, sp, lsr #13
mov	\rd, \rd, lsl #13
.endm

linux thread_info内核栈是在同一块内存中,一般设置为8K,所以把内核栈8K字节对齐以后,就能获取到thread_info的地址,并把该地址放入tsk寄存器中

(3)adr    tbl, sys_call_table

看一下系统调用表是如何定义的,在arch/arm/kernel/entry-common.S中:

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

主要是calls.S这个头文件,比较有意思的是,在entry-common.S文件前面的部分,有这样一段代码:

.equ NR_syscalls,0
#define CALL(x) .equ NR_syscalls,NR_syscalls+1
#include "calls.S"

/*
 * Ensure that the system call table is equal to __NR_syscalls,
 * which is the value the rest of the system sees
 */
.ifne NR_syscalls - __NR_syscalls
.error "__NR_syscalls is not equal to the size of the syscall table"
.endif

#undef CALL
#define CALL(x) .long x

先定义了CALL(x)为NR_syscalls,NR_syscalls+1,然后#include "calls.S",calls.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)
/* 10 */	CALL(sys_unlink)
。。。。。。。。。。。。。。。。。。。。。。。。。。。。

所以第一次#include "calls.S"时只是计算了最大的系统调用数,并把该值赋值给NR_syscalls,然后重新定义了CALL(x) .long x,在后面的sys_call_table中#include "calls.S"的时候,才真正去初始化了系统调用表。

(4)stmdb    sp!, {r4, r5}

做完4以后,sp的状态如下:

高地址
---------------------------------------------------陷入内核时sp的位置
S_OLD_R0,	
S_PSR,	
S_PC,	
S_LR,	
S_SP,	
S_IP,	
S_FP,	
S_R10,	
S_R9,	
S_R8,	
S_R7,	
S_R6,	
S_R5,	
S_R4,	
S_R3,	
S_R2
S_R1
S_R0	
R4
R5
----------------------------------------------------sp现在的位置
低地址

(5)ldrcc    pc, [tbl, scno, lsl #2]   

去执行具体的系统调用函数

2.2系统调用返回

系统调用处理 完以后,从ret_fast_syscall返回:

ret_fast_syscall:
 UNWIND(.fnstart	)
 UNWIND(.cantunwind	)
	disable_irq			//关闭中断	@ disable interrupts
	ldr	r1, [tsk, #TI_FLAGS]  //从thread info中获取#TI_FLAGS,
	tst	r1, #_TIF_WORK_MASK  //返回user之前,_TIF_WORK_MASK 中的位如果有值,则需要去处理相关任务
	bne	fast_work_pending   //先不返回,去处理pending的相关事务,比如系统调度
	asm_trace_hardirqs_on

	/* perform architecture specific actions before user return */
	arch_ret_to_user r1, lr    //这两个命令不是重点
	ct_user_enter

	restore_user_regs fast = 1, offset = S_OFF //恢复用户寄存器,并返回用户态
---------------------------------------(1)
 UNWIND(.fnend		)

/*
 * Ok, we need to do extra processing, enter the slow path.
 */
fast_work_pending:   //penging相关的处理,暂时不分析
	str	r0, [sp, #S_R0+S_OFF]!		@ returned r0
work_pending:
	mov	r0, sp				@ 'regs'
	mov	r2, why				@ 'syscall'
	bl	do_work_pending
	cmp	r0, #0
	beq	no_work_pending
	movlt	scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
	ldmia	sp, {r0 - r6}			@ have to reload r0 - r6
	b	local_restart			@ ... and off we go

(1)restore_user_regs fast = 1, offset = S_OFF

S_OFF的值为8,返回的代码处理都在该宏里面:

.macro	restore_user_regs, fast = 0, offset = 0
	ldr	r1, [sp, #\offset + S_PSR]	//之前sp的位置偏移了8,所以这边要偏移要8+S_PSR才能找到原来存放用户态cpsr的位置@ get calling cpsr
	ldr	lr, [sp, #\offset + S_PC]!	//获取pc,并且更新了sp的位置到S_PC@ get pc
	msr	spsr_cxsf, r1		//把用户态cpsr放入svc 模式的spsr中,为返回用户态做准备@ save in spsr_svc
#if defined(CONFIG_CPU_V6)
	strex	r1, r2, [sp]			@ clear the exclusive monitor
#elif defined(CONFIG_CPU_32v6K)
	clrex					@ clear the exclusive monitor
#endif
	.if	\fast
	ldmdb	sp, {r1 - lr}^		//fast为1,走这边,恢复用户态寄存器	@ get calling r1 - lr
	.else
	ldmdb	sp, {r0 - lr}^			@ get calling r0 - lr
	.endif
	mov	r0, r0				@ ARMv5T and earlier require a nop
						@ after ldm {}^
	add	sp, sp, #S_FRAME_SIZE - S_PC  //设置内核态sp的位置为进入内核态时候的位置,所以每次进入内核态,内核栈总是干净的
	movs	pc, lr		//把lr赋值给pc,执行陷入内核的下一条指令,同时s符号会把svc模式下的spsr自动压入cpsr,实现模式切换,进入用户态		@ return & move spsr_svc into cpsr
	.endm

2.3 具体内核系统调用函数

以sys_open为例,介绍一下内核的系统调用函数。

上面在陷入内核以后,具体的系统调用处理函数则为:sys_open。

内核并没有直接定义sys_open。

该函数声明如下:

asmlinkage long sys_open(const char __user *filename,
                int flags, int mode);

在内核源码中直接搜索sys_open,无法搜到它的实现代码,实际上它是在fs/open.c中实现的:

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是一个宏:

syscalls.h (include\linux)

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

SYSCALL_DEFINEx也是一个宏:

syscalls.h (include\linux)

#define SYSCALL_DEFINEx(x, sname, ...)                \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

__SYSCALL_DEFINEx仍然是个宏:

syscalls.h (include\linux)

#define __SYSCALL_DEFINEx(x, name, ...)                    \
    asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))

所以展开后的结果就是:

asmlinkage long sys_open(__SC_DECL3(__VA_ARGS__))

其中,__SC_DECL3定义如下:

syscalls.h (include\linux)

#define __SC_DECL1(t1, a1)    t1 a1
#define __SC_DECL2(t2, a2, ...) t2 a2, __SC_DECL1(__VA_ARGS__)
#define __SC_DECL3(t3, a3, ...) t3 a3, __SC_DECL2(__VA_ARGS__)

所以最终的结果如下:

asmlinkage long sys_open(const char __user *filename, int flags, int mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;

	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

 

2019-10-27 18:21:38 hongweisong666 阅读数 254
  • 《C语言/C++学习指南》Linux开发篇

    本篇面向Linux初级、但已掌握C/C++基本语法的学员,以CentOS为演示平台,介绍Linux下的C/C++开发技术。在内容编排上,先介绍Linux的安装、基本使用和配置,然后再开始介绍在Linux下的C/C++开发技术: gcc命令行、Makefile、gdb调试方法、动态库与静态库、pthread线程、进程、进程间通信、系统调用及杂项技术。(前四章免费)

    59609 人正在学习 去看看 邵发

    一 、实验目的

  1. 了解Linux系统调用的工作机制
  2. 掌握Linux系统调用的实现方法
  • 二、 实验内容

  1. 实现一个新的“Hello World”系统调用。
  2. 验证系统调用
    1. 创建一个用户程序来调用“Hello World”系统调用:
    2. 将用户程序编译后,放入rootfs.img根文件系统中。
    3. 启动qemu虚拟机,在虚拟机中运行用户程序。成功时将能看到“Hello World!”在虚拟机终端输出
  • 三 、实验过程及结果

            一、首先在系统调用表中添加新的系统调用编号,如图所示

      二、实现系统调用程序文件路径

                                        

    三、重新编译内核

        执行make -j4命令来编译内核,由于之前涉及到makefile,所以这次编译的时间编译的时间比较短

  四、验证系统调用

   Ⅰ、这一部分的话,在一个文件夹里面添加一个hello.c文件,内容如图所示

                            

                  

Ⅲ、将第二步骤编写出的a.out文件move到根文件系统中,在这之前要把之前的根文件系统重新挂载起来,等到move到根文件系统后再取消挂载。

Ⅳ、启动qemu虚拟机,在虚拟机中运行用户程序

   1)在这一步的时候可能会出现login的问题,只要把之前根文件系统的/etc/inittab里面变成::respawn:-/bin/sh ,即可解决有login的问题

   2)启动qemu虚拟机,在虚拟机中运行用户程序

                         

思考题:

                   

                  为了向用户空间写入数据,内核提供了copy_to_user(),他需要三个参数,第一个参数是进程空间中的目的内存地                  址,第二个是内核空间内的源地址,第三个参数是需要拷贝的数据的长度(字节数)。

 

        为了从用户空间读取数据,内核提供了copy_from_user(),他和copy_to_user()相似,该函数把第二个参数指定位置上      的数据拷贝到第一个参数指定的位置上,拷贝的数据的长度由第三个参数指定。

        如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,则返回0。当出现上述错误的时候,系统调用返回标准-EFAULT。

          copy_to_user()和copy_from_user()函数都有可能引起阻塞。如果用户空间的数据地址是个非法的地址,或是超出用户  空间的范围,当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生进程就会休眠,知道缺页处理程序将该页从硬盘换回到物理内存。或是那些地址还没有被映射到,都可能对内核产生很大的影响。

                 如果成功则调用__copy_from_user函数开始拷贝数据了,如果失败的话,就把从to指针指向的内核空间地址到to+size        范围填充   

    这里先判断要拷贝的字节大小,如果是8,16,32大小的话,则调用__get_user_size来拷贝数据.

 

  • 四、总结

         系统调用有open,read ,write等。一般的话,使用SYSCALL_DEFINEx来定义系统调用,但是本质上还是sys_xxx的模式。系统调用在用户态,最终都会调用到同样的一组接口,从_syscall0到_syscall6。这一组接口基本都是类似的,只不过向内核传递的参数个数不一样。_syscall后面的数字就代表传递参数的个数。可以看到只有_syscall6多了栈操作,是因为系统调用前5个参数是通过%ebx, %ecx, %edx, %esi, %edi,五个寄存器传递的,超过五个参数,就只能通过栈来传递参数了,实验做完之后了解了linux系统的调用过程,以及用户空间写入数据和读出数据的过程。

2019-11-03 23:24:16 darker0019527 阅读数 23
  • 《C语言/C++学习指南》Linux开发篇

    本篇面向Linux初级、但已掌握C/C++基本语法的学员,以CentOS为演示平台,介绍Linux下的C/C++开发技术。在内容编排上,先介绍Linux的安装、基本使用和配置,然后再开始介绍在Linux下的C/C++开发技术: gcc命令行、Makefile、gdb调试方法、动态库与静态库、pthread线程、进程、进程间通信、系统调用及杂项技术。(前四章免费)

    59609 人正在学习 去看看 邵发

本文以write()来简要分析一下Linux系统调用过程:

write系统调用:
函数定义如下:

在这里插入图片描述lib/write.c

其中_syscall是一个宏定义, 内容类似如下:

在这里插入图片描述
include/unistd.h

其实就相当于int write(int fd, const char *buf, off_t count) { … }
就是定义了write()这个函数。

可以看到,在执行write函数时,其实就是执行了int 0x80, 使用了0x80中断来实现系统调用,因为write函数调用的时候是在用户态的,而write要做的是把字符串写入到指定文件中,比如写到显存中,写入显存这个操作是需要内核态,也就是0特权级下才能进行的(当然要看显存对应选择子的DPL),也就是说现在在用户态完成不了这个写入显存的操作,怎么办?当然是先切换特权级到内核态,等完成了0特权级完成了操作后再回到用户态即可,int 0x80就完成了用户态向内核态的切换。

Linux中不止一个write调用,还有open, read…等,那int 0x80如何确定是调用哪一个系统函数呢?
int 0x80这个中断也有“参数”, 上面"0" (__NR_name) 部分就是int 0x80的参数,0x80中断会根据__NR_name这个下标来调用与write对应的系统调用,为什么说__NR_name是下标呢,既然说它是下标,那么就会有一个数组。是的,Linux内部维护了一个系统调用的数组,如下:

在这里插入图片描述
include/linux/sys.h

这些下标定义如下:

在这里插入图片描述
include/unistd.h

可以看到,里面存放了Linux所支持的所有系统调用(内核态函数的指针),而__NR_name就是每个用户态函数在这个数组对应内核态函数的下标。比如write()对应的内核态函数为sys_write,当调用write时,最终会调用到上面这个sys_call_table中的sys_write函数,下面看下是怎么通过int 0x80和他的参数__NR_name来调用对应的内核态函数的。

在这里插入图片描述
kernel/sched.c

可以看到,在初始化的时候,int 0x80对应的处理函数为system_call, 当调用int 0x80时,会执行system_call这个函数,如下:

在这里插入图片描述
kernel/sys_call.s

看上面99行,就是调用了sys_call_table中相应的项,其中eax中的值就是上面的__NR_name,(数组的下标), 比如write()调用到system_call时,__NR_write = 4, sys_call_table第5荐正好为sys_write, 所以就执行了相应的内核态函数(执行内核态才能执行的操作)。

总结:

Linux实现系统调用首先要有一个表来存放所有与用户态函数对应的系统态的函数(sys_call_table), 调用过程:user_func() --> int 0x80 (user_func对应的系统态函数在表中的下标, 记为index), 此时已经进入内核态 --> system_call(index) --> sys_func();


Linux源码版本: 0.12

2014-05-05 10:28:48 GetNextWindow 阅读数 1423
  • 《C语言/C++学习指南》Linux开发篇

    本篇面向Linux初级、但已掌握C/C++基本语法的学员,以CentOS为演示平台,介绍Linux下的C/C++开发技术。在内容编排上,先介绍Linux的安装、基本使用和配置,然后再开始介绍在Linux下的C/C++开发技术: gcc命令行、Makefile、gdb调试方法、动态库与静态库、pthread线程、进程、进程间通信、系统调用及杂项技术。(前四章免费)

    59609 人正在学习 去看看 邵发

写这篇文章是看到一个as汇编器里编写的汇编代码,有一个指令int 80h没有搞懂,然后自己查资料发现不少东西,本文旨在浅显的分析linux的系统调用,主要是linux0.12内核来说!

目前操作系统内核的结构模式主要分为整体式的单内核和层次式的微内核模式。而0.12内核是单内核模式。在单内核模式的系统中,操作系统提供服务的流程:应用程序使用指定的参数值执行系统调用指令(int 80h),使CPU从用户态切换到内核态,然后操作系统根据具体的参数值调用特定的服务程序,而这些服务程序则根据需要再调用底层的一些支持函数以完成特定的功能。在完成了应用程序要求的服务之后,操作系统又降CPU从核心态切换回用户态,从而返回到应用程序中继续执行后面的指令。因此概要的讲,单内核模式的内核分为3个层次:调用服务的应用程序、执行系统调用的服务层、支持系统调用的底层函数。

1.系统调用接口

系统调用(通常称为syscalls)接口是Linux内核和上层应用程序进行交互通信的唯一接口。应用程序、库函数和内核系统调用的关系如下:


用户程序通过直接或间接(通过库函数)调用中断int 0x80,并在寄存器eax中指定系统调用功能号,即可使用内核资源,包括系统硬件资源。不过通常应用程序都是使用具有标准接口定义的C函数库中的函数间接地使用内核的系统调用。通常,系统调用使用函数形式进行调用,因此可以带有一个或者多个参数。对于系统调用执行的结果,它会在返回值中表示出来。

在Linux内核中,每个系统调用都有一个唯一的系统调用功能号。这些功能号定义在include/unistd.h。例如write系统调用的功能号是4,定义为符号__NR_write。这些系统调用功能号实际上对应于include/linux/sys.h中定义的系统调用处理程序指针数组表sys_call_table[]中项的索引。因此write系统调用用的处理程序指针就位于该数组的第四项。

当我们想在自己的程序中使用这些系统调用符号,需要像下面所示在包括文件<unistd.h>之前定义符号_LIBRARY。另外我们从sys_call_table[]中可以看出,内种中所有系统调用处理函数的名称基本上都是以符号sys_开始的。例如系统调用read在内核源码中实现函数是sys_read()。

2.系统调用处理过程

当应用程序经过库函数向内核发出一个中断调用int 0x80时,就开始一个系统调用。其中寄存器eax中存放着系统调用号(比如调用write,eax就存储4),而携带的参数一次放在寄存器ebx(第一个参数)、ecx(第二个参数)、edx(第二个参数)中,因此Linux0.12内核中用户程序可以向内核最多传递3个参数,当然也可以不带参数。处理系统调用中断0x80的过程是在程序kernel/system_call.s中的system_call。

为了方便执行系统调用,内核源码在include/unistd.h文件中定义了宏函数_syscalln(),其中n代表携带参数的个数,可以是0-3。

例如对于read()系统调用,其定义是int read(int fd,char *buf,int n)

若在用户程序中直接执行对应的系统调用,那么该系统调用的宏的形式如下:

#define _LIBRARY_

#include<unistd.h>

_syscall3(int,read,int,fd,char*,buf,int,n)

因此,我们可以在用户程序中直接使用上面的_syscall3()来执行一个系统调用read(),而不用通过C函数库作中介。实际上,C函数库中函数最终调用的形式和这里给出的完全一样。这里我们看到,每个系统调用宏都有2+2*n个参数,第一个参数对应返回值类型,第二个参数是系统调用的名称,随后是系统调用所携带参数的类型和名称,这个宏会被扩展成包含内嵌汇编的c函数,如下所示:

int read(int fd,char *buf,int n)
{
	long __res;
	__asm__ volatile(
		"int $0x80"
		:"=a" (__res)
		:"0" (__NR_read),"b" ((long)(fd),"c" ((long)(buf),"d" ((long)(n)));
		if(__res>=0)
			return int __res;
		errno=-__res;
		return -1;
}

可以看出这个宏经过展开就是一个读系统调用的具体实现。其中使用嵌入式汇编语法以功能号__NR_read(3)执行了Linux系统中断调用0x80。该中断调用在eax(__res)寄存器中返回了实际读取的字节数。

可以看出,这个宏经过展开就是一个读操作系统调用的具体实现。其中使用了嵌入式汇编语句以功能号_NR_read(3)执行了Linux的系统中断调用0x80,当进入内核中的系统调用处理程序kernel/sys_call.s后,system_call的代码会首先检查eax中的系统调用功能号是否在有效系统调用号范围内,然后根据sys_call_table[]函数指针表调用执行相应的系统调用处理程序。

call _sys_call_table(,%eax,4)

这句汇编操作数的意思是间接调用地址在_sys_call_table+%eax*4处的函数。由于sys_call_table[]指针每项4字节,因此这里需要给系统调用功能号乘以4。

Linux系统调用的内核态分析

博文 来自: willing_heart_zx
没有更多推荐了,返回首页