Linux 编程中的API函数和系统调用的关系【转】

时间:2023-01-20 08:42:11
API:(Application Programming Interface,应用程序编程接口)
  指的是我们用户程序编程调用的如read(),write(),malloc(),free()之类的调用的是glibc库提供的库函数。API直接提供给用户编程使用,运行在用户态。
  我们经常说到的POSIX(Portable Operating System Interface of Unix)是针对API的标准,即针对API的函数名,返回值,参数类型等。POSIX兼容也就指定这些接口函数兼容,但是并不管API具体如何实现。
系统调用
  通过软中断或系统调用指令向内核发出一个明确的请求,内核将调用内核相关函数来实现(如sys_read(),sys_write(),sys_fork())。用户程序不能直接调用这些Sys_read,sys_write等函数。这些函数运行在内核态。
两者关系:
  通常API函数库(如glibc)中的函数会调用封装例程,封装例程负责发起系统调用(通过发软中断或系统调用指令),这些都运行在用户态。内核开始接收系统调用后,cpu从用户态切换到内核态(cpu处于什么状态,程序就叫处于什么状态,所以很多地方也说程序从用户态切换到内核态,实际是cpu运行级别的切换,通常cpu 运行在3级表示用户态,cpu 运行在0级表示内核态),内核调用相关的内核函数来处理再逐步返回给封装例程,cpu进行一次内核态到用户态的切换,API函数从封装例程拿到结果,再处理完后返回给用户。
 但是API函数不一定需要进行系统调用,如某些数学函数,没有必要进行系统调用,直接glibc里面就给处理了,整个过程运行在用户态。
  所以作为我们编写linux用户程序的时候,是不能直接调用内核里面的函数的,内核里面的函数位于进程虚拟地址空间里面的内核空间,用户空间函数及函数库都处于进程虚拟地址空间里面的用户空间,用户空间调用内核空间的函数只有一个通道,这个通道就是系统调用指令,所以通常要调用glibc等库的接口函数,glibc也是用户空间的,但glibc自己实现了调用特殊的宏汇编系统调用指令进行cpu运行状态的切换,把进程从用户空间切换到内核空间。
用户态函数执行全过程(这里只讲需要进行系统调用的函数)Linux 编程中的API函数和系统调用的关系【转】
  用户态xyz()函数,内核最终一般会调用形如sys_xyz()的服务例程来处理(不过也有一些例外,这里暂时不考虑)
  函数xyz()是直接提供给用户编程使用的。图中“SYSCALL”,“SY***IT”表示真正的汇编指令(汇编指令具体调用的是哪个暂时不关心,我们只需在此关注发起和退出了一个系统调用)。
 发起系统调用:xyz()函数执行的过程中会执行SYSCALL汇编指令,此指令将会把cpu从用户态切换到内核态。SYACALL汇编指令中会包含将要调用的内核函数的系统调用号和参数,内核在上图系统调用处理程序中去查一个sys_call_talbe数组来找到这个系统调用号对应的服务例程(如sys_xyz())函数的地址,然后调用这个地址的函数执行。(这里glibc里面的系统调用号和内核里面的系统调用号必须完全相等,当然,这是约定好的)
  系统调用返回:服务例程(如sys_xyz())函数返回值一般返回正数和0表示系统调用成功结束,而负数表示一个出错条件。紧接着SY***IT退出系统调用,此指令将cpu从内核态切换到用户态,glibc针对系统调用返回值如果出错则需要设置好errno(通常在c库头文件/usr/include/errno.h中),然后返回一个值做为glibc封装例程的返回值(如xyz()的返回值)。这里errno是libc自己用来定义的出错码,不一定是最后gblic封装例程的返回值
 

这里涉及到几个概念需要好好讲讲:

1.系统调用号

为了把系统调用号和相应的服务例程关联起来,如64位系统中
cat /usr/include/asm/unistd_64.h
#ifndef __SYSCALL
#define __SYSCALL(a, b)
#define __NR_read                               0
__SYSCALL(__NR_read, sys_read)
#define __NR_write                              1
等等
__SYSCALL(__NR_write, sys_write)
#define __NR_dup                                32
__SYSCALL(__NR_dup, sys_dup)
#define __NR_dup2                               33
__SYSCALL(__NR_dup2, sys_dup2)
等等
Glibc和内核里面的这个系统调用号是一致的,所以glibc调用汇编之类把系统调用号传给内核的时候,内核通过自己的系统调用分派表sys_call_table(可以理解为一个系统调用号,对应一个函数入口地址)找到这个具体的系统调用服务例程对应的函数入口地址,如上面sys_read,sys_write等

2.参数传递

在发起系统调用前,eax寄存器里面存储了系统调用号。如用户程序fork()函数,glibc 发出int 0x80或sysenter指令前,eax寄存器就会设置好内核的sys_fork函数对应的系统调用号,这是glibc里面的封装例程会自动设置好的,程序员无需关心。
    有些系统调用可能调用很多参数(除了系统调用号之外),普通c函数的参数传递是通过把参数值写入活动的程序栈(用户态栈或者内核态栈)实现的。因为系统调用是一种跨用户态和内核态的特殊函数,所以这两个栈都不能用。在发出系统调用之前,系统调用的参数写入了cpu的寄存器(如glibc去写好这些寄存器),然后发出系统调用之后,而在内核调用服务例程(如sys_fork()服务例程)之前,内核再把存放在cpu中的参数拷贝的内核态的堆栈中(因为sys_fork只是普通的c函数,前面说过普通c函数的参数传递是通过把参数值写入活动的程序栈(用户态栈或者内核态栈)实现的)。内核为什么不直接把用户态的栈拷贝到内核态的栈而要去通过寄存器来传呢?首先,同事操作两个栈是比较复杂的,其次,寄存器的使用使得系统调用处理程序的结构与其它异常处理程序的结构类似。
    使用寄存器传递参数,必须满足两个条件:
    每个参数的长度不能超过寄存器的长度(比如寄存器长度32位,那参数长度就不能超过32位);
    参数的个数不能超过6个(除了eax中传递的系统调用号),因为80x86处理器的寄存器的数量是有限的。
    第一个条件总能成立,因为POSIX标准规定,如果寄存器里面装不下那个长度的参数,那么必须改用参数的地址来传递。
    第二个条件有的系统调用参数大于6个,这种情况下,必须用一个单独的寄存器执行进程地址空间的这些参数所在的一个内存区。
    存放系统调用号和系统调用参数的寄存器是eax(存放系统调用号),ebx,ecx,edx,esi,edi,ebp

3.SYSCALL,SY***IT进入退出系统调用

这里SYSCALL,SY***IT只是个代号,具体汇编指令如下
    进入系统调用:内核2.6以前通过int $0x80汇编指令;内核2.6以后sysenter汇编语言指令。
    退出系统调用:旧的iret汇编指令,新的sy***it指令

现在内核同时支持这两类新旧指令
    向量128(0x80)对应于内核入口点,在内核初始化期间调用的函数trap_init(0,用以下方式建立对应于向量128的中断描述符表项set_system_gate(0x80,&system_call).
    当用户态进程发出int $0x80指令时,cpu切换到内核态并开始从地址system_call处开始执行指令。System_call()函数首先把系统调用号和这个异常处理程序可以用到的所有cpu寄存器保存到相应的内核栈中,不包括由控制单元已自动保存的eflags,cs,eip,ss,esp寄存器。随后,在ebx中存放当前进程的thread_info数据结构的地址,这是通过获得内核栈指针的值并把它取整到4kb或8kb的倍数而完成的。然后检查thread_info结构flag字段的TIF_SYSCALL_TRACE和TIF_SYSCALL_AUDIT标识之一是否被设置为1,也就是检查是否有某一调试程序正在跟踪执行程序对系统调用的调用。如果被置1,那么system_call()函数两次调用do_syscall_trace()函数:一次正好在这个系统调用服务例程执行之前,一次在其之后。Do_syscall_trace函数停止current,并因此允许调试进程收集关于current的信息。
    系统调用退出:
    (1)用户态的寄存器刚进来到系统调用的时候被保存到了内核栈中,错误的返回值会被写的刚开始传递系统调用号的那个eax寄存器所在栈的位置。(那么将来当用户态恢复执行的时候,eax寄存器里面的内容就是系统调用的返回码了。)
    (2)禁止本地中断,并检查current的thread_info结构中的标志。如果有任何标志被设置,那么在返回到用户态之前还需要完成一些工作。

用source insight在linux内核中查找sys_open函数
#define __NR_open                               2
__SYSCALL(__NR_open, sys_open)
刚开始我搜索“sys_open”,苦逼的找了几遍没找到具体实现的地方,都是调用这个函数的地方。。后经伯松提醒,可能被宏给替换了。。后想起代码中出现过的SYSCALL_DEFINE宏,就进行了给name加上”sys_”前缀,所以找到SYSCALL_DEFINE中含有open的这句

点击(此处)折叠或打开

  1. SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
  2. {
  3. long ret;
  4. if (force_o_largefile())
  5. flags |= O_LARGEFILE;
  6. ret = do_sys_open(AT_FDCWD, filename, flags, mode);
  7. /* avoid REGPARM breakage on x86: */
  8. asmlinkage_protect(3, ret, filename, flags, mode);
  9. return ret;
  10. }
深刻怀疑SYSCALL_DEFINE,其定义如下(在include/linux/syscalls.h中)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)  //注意,这里name已经变成了”_name”,加上了下划线了,所以”open”变成了“_open”了

点击(此处)折叠或打开

  1. #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
  2. #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
  3. #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
  4. #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
  5. #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
进一步
#define SYSCALL_DEFINEx(x, sname, ...)    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)//前面name已经加上“_open”了,记住
进一步
#define __SYSCALL_DEFINEx(x, name, ...)   asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))//这里,变成了“sys_open”了
所以,这里变成了 asmlinkage long sys_open(__SC_DECL3(__VA_ARGS__))
而再进一步
#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,__SC_DECL2(__VA_ARGS__))
进一步变成了
asmlinkage long sys_open(const char __user*  fliename,int flags,__SC_DECL1(t1,a1))
进一步变成了
asmlinkage long sys_open(const char __user*  filename,int flags,umode_t mode)了!
所以,最终变成了

点击(此处)折叠或打开

  1. asmlinkage long sys_open(const char __user* filename,int flags,umode_t mode){
  2. long ret;
  3. if (force_o_largefile())
  4. flags |= O_LARGEFILE;
  5. ret = do_sys_open(AT_FDCWD, filename, flags, mode);
  6. /* avoid REGPARM breakage on x86: */
  7. asmlinkage_protect(3, ret, filename, flags, mode);
  8. return ret;
  9. }
补充说明:宏定义中出现的#,##,...和__VA_ARGS_的特殊说明
#  
这个是一个字符串替换,如#define S(x)   “a”#x
S(1),则变成了字符串“a1”了
##,这个是个简单的替换
如#define T(x)  a##x
T(1)则变成了 a1 了,比如你前面定义了int a1=2; 就可以printf”%d”,T(1)),即等价于printf(“%d”,a1);如果你用来上面的S(1)替换T(1)那就变成了printf(“%d”,“a1”)肯定就不对了,或者你#define S(x) a#x,用S(1)替换T(1)那就变成了printf(“%d”,a”1”)了,肯定也不对了,所以,用“##”有的时候也是必须 的

宏定义的参数,如#define X(...) printf(“%s %s”,__VA_ARGS__);
X(“a”,”b”)就变成了printf(“%s %s”,a,b);了
__VA_ARGS__就表示吧...参数给完整替换掉,”__VA_ARGS__”这个字符串缺一个字符都不可以。。
另外“#,##,...和__VA_ARGS_”也可参看http://www.cnblogs.com/zhujudah/archive/2012/03/22/2411240.html
的一些讲解。
测试例程:

点击(此处)折叠或打开

  1. #include
  2. #define S(x) "a"#x
  3. #define T(x) a##x
  4. #define M(x) x
  5. #define N(x) (x)
  6. int main(){
  7. int a1=2;
  8. printf("%d\n",T(1));
  9. printf("%s\n",S(1));
  10. printf("%d\n",M(1));
  11. printf("%d\n",N(1));
  12. }
输出
2
a1
1
1
可以看到宏中带括号和不带括号是一样的效果
 
参考
1.《深入理解linux内核(第三版)》,
2.  kernel-3.6.7源码
3.http://www.cnblogs.com/zhujudah/archive/2012/03/22/2411240.html