你所不知道的关于库函数和系统调用的那些事

news2024/11/28 20:56:37

系统调用和库函数的区别

相信大家在面试或者刷面试题的时候经常能看到这样的问题,“简述一下系统调用和库函数的区别”。

系统调用是操作系统提供给用户的接口,能让用户空间的程序有入口访问内核。而库函数数一组标准函数,比如复合 POSIX 或者 sysv 标准的函数。
在 linux 内核中,系统调用是专门提供给用户态程序调用的接口,内核通常是不会主动调用这些函数的。而不同操作系统中系统调用的实现都不相同。
库函数遵循标准,主要是为了考虑移植性问题。同时,库函数大多都有缓存机制,且有些库函数会调用系统调用来实现。我们看下 《Expert C Programming》 一书中的教科书式的回答。

库函数系统调用
所有的 ANSI C 编译器版本中,C 函数库都是相同的各个操作系统的系统调用是不同的
它调用函数库中的一个程序它调用系统内核的服务
在用户地址空间执行在内核地址空间执行
它的运行时间属于 ”用户“时间它的运行时间属于 ”系统“时间
属于过程调用,开销较小需要切换到内核上下文环境中然后再切换回来,开销较大
在 C 函数库libc中有大约300多个程序在 UNIX 中大约有 90 个系统调用(MS-DOS 中少一些)
记录与 UNIX OS man page 的第二节记录与 UNIX OS man page 的第三节
典型的 C 函数库调用:fopen, system, fprintf典型的系统调用:open, chdir, write, fork, brk

库函数调用通常比行内展开的代码慢(可以理解成内联), 这是因为存在函数调用开销。但是系统调用需要从用户态切换到内核态,再切换回用户态的过程,会比库函数调用还慢。

特别需要注意一点,system 是库函数而不是系统调用。

以上列出的这个区别,应该是很完善的答案了,如果在面试环节遇到这个问题,这么回答肯定是不错的。那么,通常我们在 linux 系统中看到的 manpage 的 第 2 章节,就是系统调用的介绍,第三章节就是库函数的介绍,那么分别调用这两个章节的函数的话,比如

int open(const char *pathname, int flags);
FILE *fopen(const char *path, const char *mode);

那编译器在编译的时候是如何处理的呢?系统调用是操作系统提供的接口的话,编译器在编译的时候需要链接吗?

我们来浅浅的分析一下。

实例解析

我们来看一个简单的 c 代码的例子

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define FILENAME "test.txt"

void test_system_call() {
  int fd = open(FILENAME, O_RDWR);
  close(fd);
}

void test_standard_libs() {
  FILE* fp = fopen(FILENAME, "rw");
  fclose(fp);
}

int main(int argc, char *argv[])
{
  test_system_call();
  test_standard_libs();
  return 0;
}

上面这个程序,分别调用了系统调用 open 和标准库函数 fopen。可以通过 man 2 open 和 man 3 fopen 看下这两个函数的详细介绍。我们先看下 man man 中对章节的介绍。

DESCRIPTION
       man is the system's manual pager.  Each page argument given to man is normally the name of a program, utility or function.  The manual page asso‐
       ciated  with  each  of  these arguments is then found and displayed.  A section, if provided, will direct man to look only in that section of the
       manual.  The default action is to search in all of the available sections following a pre-defined order ("1 n l 8 3 2 3posix 3pm 3perl 5 4 9 6 7"
       by  default,  unless  overridden  by the SECTION directive in /etc/manpath.config), and to show only the first page found, even if page exists in
       several sections.

       The table below shows the section numbers of the manual followed by the types of pages they contain.

       1   Executable programs or shell commands
       2   System calls (functions provided by the kernel)
       3   Library calls (functions within program libraries)
       4   Special files (usually found in /dev)
       5   File formats and conventions eg /etc/passwd
       6   Games
       7   Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
       8   System administration commands (usually only for root)
       9   Kernel routines [Non standard]

可以看到,第二章节就是系统调用,第三章节就是库函数。

使用 gcc 进行编译,使用 debug 模式

gcc -g test.c -o test

用 readelf 看下符号

$ readelf -sW test
Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fclose@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND close@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND open@GLIBC_2.2.5 (2)
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fopen@GLIBC_2.2.5 (2)
......

结果是不是跟想象中的有点不太一样。我们发现,无论是 fopen 还是 open 都是 GLIBC 的符号。也就是说,这里所谓的系统调用 open 函数,其实仅仅是 libc 中的一个函数定义。

换句话说,manpage 中的第二章节,是一个系统调用的描述,封装了对kernel系统调用的接口。

The section describes all of the system calls(requests for kernel to perform operations).

而编译后的二进制文件 test 也仅仅依赖 libc.so 库

$ readelf -dW test | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

从这里可以看出,编译器在编译时,只需要知道 open 或者 fopen 的头文件,这些头文件是 glibc 提供的。在链接器链接时,这些函数实际的定义都是在 libc.so 中,通过共享库的链接方式进行链接,这些符号都是动态符号,需要进行地址重定位的,而跟kernel没什么关系。

那么 libc 中的描述的系统调用到底是什么呢,我们来看下 open 这个系统调用。

open 在 libc 中的实现

sysdeps/unix/sysv/linux/open.c 有如下实现

/* Open FILE with access OFLAG.  If O_CREAT or O_TMPFILE is in OFLAG,
   a third argument is the file protection.  */
int
__libc_open (const char *file, int oflag, ...)
{
  int mode = 0;

  if (__OPEN_NEEDS_MODE (oflag))
    {
      va_list arg;
      va_start (arg, oflag);
      mode = va_arg (arg, int);
      va_end (arg);
    }

  return SYSCALL_CANCEL (openat, AT_FDCWD, file, oflag, mode);
}

weak_alias (__libc_open, open)

weak_alias 是一个宏,用于创建弱符号别名。这里是将 __libc_open 这个符号创建为 open 的弱别名。也就是说,如果其他模块提供了 open 的实现,那么在链接时,链接器会使用该版本的 open 实现而不是 __libc_open

__libc_open 中,调用了宏 SYSCALL_CANCEL,该宏在 sysdeps/unix/sysdep.h 中进行了定义。

#define SYSCALL_CANCEL(...) \
  ({									     \
    long int sc_ret;							     \
    if (NO_SYSCALL_CANCEL_CHECKING)					     \
      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;								     \
  })

核心调用就是 INLINE_SYSCALL_CALL,我用来分析下 open 这个实现中整个宏展开的一个过程。

INLINE_SYSCALL_CALL (__VA_ARGS)
==> INLINE_SYSCALL_CALL(openat, AT_FDCWD, file, oflag, mode)

这些宏定义在 sysdeps/unix/sysdep.h 中可以找到,

INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode)
==> __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, openat, AT_FDCWD, file, oflag, mode)
==> __SYSCALL_CONCAT (__INLINE_SYSCALL, __INLINE_SYSCALL_NARGS(openat, AT_FDCWD, file, oflag, mode))(openat, AT_FDCWD, file, oflag, mode)

来分析一下 __INLINE_SYSCALL_NARGS 这个宏

#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,)

这个宏的作用是计算参数的个数,数字和字母参数就是占位符的作用。把上面的宏展开就是

__INLINE_SYSCALL_NARGS(openat, AT_FDCWD, file, oflag, mode)
==> __INLINE_SYSCALL_NARGS_X (openat, AT_FDCWD, file, oflag, mode,7,6,5,4,3,2,1,0,)

参数对应关系如下所示

a -> openat
b -> AT_FDCWD
c -> file
d -> oflag
e -> mode
f -> 7
g -> 6
h -> 5
n -> 4

n 就是最终结果,为 4。所以上面的宏继续展开就是

__SYSCALL_CONCAT (__INLINE_SYSCALL, __INLINE_SYSCALL_NARGS(openat, AT_FDCWD, file, oflag, mode))(openat, AT_FDCWD, file, oflag, mode)
==> __SYSCALL_CONCAT (__INLINE_SYSCALL, 4)(openat, AT_FDCWD, file, oflag, mode)
==> __INLINE_SYSCALL4 (openat, AT_FDCWD, file, oflag, mode)
==> INLINE_SYSCALL (openat, 4, AT_FDCWD, file, oflag, mode)

sysdeps/unix/sysv/sysdep.h 中可以找到

/* Define a macro which expands into the inline wrapper code for a system
   call.  It sets the errno and returns -1 on a failure, or the syscall
   return value otherwise.  */
#undef INLINE_SYSCALL
#define INLINE_SYSCALL(name, nr, args...)				\
  ({									\
    long int sc_ret = INTERNAL_SYSCALL (name, nr, args);		\
    __glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (sc_ret))		\
    ? SYSCALL_ERROR_LABEL (INTERNAL_SYSCALL_ERRNO (sc_ret))		\
    : sc_ret;								\
  })

INLINE_SYSCALL 也是一个封装的宏函数,关键调用的是 INTERNAL_SYSCALL 这个宏函数。我们看下 arm 架构下这个宏的实现。在 sysdeps/unix/sysv/linux/arm/sysdep.h

#define INTERNAL_SYSCALL(name, nr, args...)			\
	INTERNAL_SYSCALL_RAW(SYS_ify(name), nr, args)

具体实现就在 INTERNAL_SYSCALL_RAW 这个宏函数中了。
请添加图片描述

可以看到,libc 中的实现,实际调用的是 syscall 汇编指令。

通过 man syscall 可以查看下简介系统调用的描述

Architecture calling conventions
       Every  architecture has its own way of invoking and passing arguments to the kernel.  The details for various architectures are listed in the two
       tables below.

       The first table lists the instruction used to transition to kernel mode, (which might not be the fastest or best way to transition to the kernel,
       so  you  might  have  to refer to vdso(7)), the register used to indicate the system call number, and the register used to return the system call
       result.

       arch/ABI   instruction          syscall #   retval Notes
       ───────────────────────────────────────────────────────────────────
       arm/OABI   swi NR               -           a1     NR is syscall #
       arm/EABI   swi 0x0              r7          r0

       arm64      svc #0               x8          x0
       blackfin   excpt 0x0            P0          R0
       i386       int $0x80            eax         eax
       ia64       break 0x100000       r15         r8     See below
       mips       syscall              v0          v0     See below
       parisc     ble 0x100(%sr2, %r0) r20         r28
       s390       svc 0                r1          r2     See below
       s390x      svc 0                r1          r2     See below
       sparc/32   t 0x10               g1          o0
       sparc/64   t 0x6d               g1          o0
       x86_64     syscall              rax         rax    See below
       x32        syscall              rax         rax    See below

这张表列出了不同系统传递给kernel的指令。在 arm/EABI 架构中,就是 swi 0x0,这与上面这个内嵌汇编中的调用是一样的。而在 arm 汇编中,@ syscall 表示注释,说明这是一条系统调用的指令。而第二张表,描述了不同架构传递给系统调用的参数所使用的寄存器。

 The second table shows the registers used to pass the system call arguments.

       arch/ABI      arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
       ──────────────────────────────────────────────────────────────────
       arm/OABI      a1    a2    a3    a4    v1    v2    v3
       arm/EABI      r0    r1    r2    r3    r4    r5    r6
       arm64         x0    x1    x2    x3    x4    x5    -
       blackfin      R0    R1    R2    R3    R4    R5    -
       i386          ebx   ecx   edx   esi   edi   ebp   -
       ia64          out0  out1  out2  out3  out4  out5  -
       mips/o32      a0    a1    a2    a3    -     -     -     See below
       mips/n32,64   a0    a1    a2    a3    a4    a5    -
       parisc        r26   r25   r24   r23   r22   r21   -
       s390          r2    r3    r4    r5    r6    r7    -
       s390x         r2    r3    r4    r5    r6    r7    -
       sparc/32      o0    o1    o2    o3    o4    o5    -
       sparc/64      o0    o1    o2    o3    o4    o5    -
       x86_64        rdi   rsi   rdx   r10   r8    r9    -
       x32           rdi   rsi   rdx   r10   r8    r9    -

我们关注 arm/EABI 架构,可以使用 7 个参数,分别对应 r0 - r6 一共 7 个寄存器。来分析下上图中的代码。_a1 对应寄存器 r0,而 _nr 表示系统调用号,对应寄存器 r7。这个系统调用号是什么意思呢?

在分析上面的宏展开时,最终调用的是

INTERNAL_SYSCALL_RAW(SYS_ify(name), nr, args)

_nr = name;

这个 name 就是 SYS_ify(name) 的值,而 SYS_ify 这个宏定义为

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

展开就是 __NR_openat,这个就是系统调用号,在 linux 系统头文件 asm-generic/unistd.h 中定义

#define __NR_openat 56

回到上面的问题。其余参数的传递就是通过

LOAD_ARGS_##nr (args)
ASM_ARGS_##_nr

来实现的,这里的 nr 的值是 4,可以从上面的宏展开分析得知。

ASM_ARGS_4 展开
==> ASM_ARGS_3, "r" (_a4)
==> ASM_ARGS_2, "r" (_a3), "r" (_a4)
==> ASM_ARGS_1, "r" (_a2), "r" (_a3), "r" (_a4)
==> ASM_ARGS_0, "r" (_a1), "r" (_a2), "r" (_a3), "r" (_a4)
==> , "r" (_a1), "r" (_a2), "r" (_a3), "r" (_a4)

这样,open 这个系统调用,使用 swi 0x0 指令,输出到 r0 寄存器对应的变量 _a1 中,_nr 对应寄存器 a7 为系统调用号,其余输入参数 _a1 - _a4 对应寄存器 r1 - r4。当调用 swi 0x0 指令时,会触发一个软中断,cpu 会暂停当前程序的执行,而跳转到 kernel 中去执行这个中断处理函数,执行相应的操作。

总结

我们通常使用的系统调用,在 manpage 第二章节所描述的函数,其实是 libc 中封装的函数,这个函数就是对应系统调用的描述,以一个 c 函数的形式提供给用户使用。而实际的实现,是在 libc 中根据特定架构提供的指令以汇编的形式实现的。比如上面分析的系统调用 open,是通过 swi 0x0 这个软中断来触发的,而系统调用号以及软中断的处理过程,是在 kernel 中实现的。

这就可以解释上面那个 test 程序了。编译器在实际编译的时候,不管是库函数 fopen 还是系统调用 open 都是当做一个外部函数符号来处理的。在链接器进行链接的时候,在 libc.so 中找到了函数定义并链接。而程序运行时,动态链接器加载 libc.so 并对 open 和 fopen 进行地址重定位,当执行 open 或者 fopen 时跳转到 libc.so 中对应的函数处执行。

今天的分享就到这里,我是猫步旅人,一个对 kernel 和编译器感兴趣的程序员。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1432565.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【论文笔记】Lift-Attend-Splat: Bird’s-eye-view camera-lidar fusion using transformers

原文链接&#xff1a;https://arxiv.org/abs/2312.14919 1. 引言 多模态融合时&#xff0c;由于不同模态有不同的过拟合和泛化能力&#xff0c;联合训练不同模态可能会导致弱模态的不充分利用&#xff0c;甚至会导致比单一模态方法性能更低。 目前的相机-激光雷达融合方法多基…

react将选中本文自动滑动到容器可视区域内

// 自动滚动到可视区域内useEffect(() > {const target ref;const wrapper wrapperRef?.current;if (target && wrapperRef) {const rect target.getBoundingClientRect();const wrapperRect wrapper.getBoundingClientRect();const isVisible rect.bottom &l…

Vision Transfomer系列第一节---从0到1的源码实现

本专栏主要是深度学习/自动驾驶相关的源码实现,获取全套代码请参考 这里写目录标题 准备逐步源码实现数据集读取VIt模型搭建hand类别和位置编码类别编码位置编码 blocksheadVIT整体 Runner(参考mmlab)可视化 总结 准备 本博客完成Vision Transfomer(VIT)模型的搭建和flowers数…

2024机械工程师面试题

1.常用的机械画图软件有哪些 SolidWorks、Pro/e、CATIA、UG、Creo、CAD、inventor。CAXA电子图板. 2.第一视角是___&#xff0c;第三视角是___&#xff1b; 只要区别是&#xff1a;物体所处的位置不同。一般中国都使用第一视角的。 3.气缸属于_____执行元件&#xff0c;电磁…

Multisim14.0仿真(五十一)基于LM555定时器的分频器设计

一、1KHz脉冲设置&#xff1a; 二、555脉冲电路&#xff1a; 三、仿真电路&#xff1a; 四、运行仿真&#xff1a;

【Linux笔记】缓冲区的概念到标准库的模拟实现

一、缓冲区 “缓冲区”这个概念相信大家或多或少都听说过&#xff0c;大家其实在C语言阶段就已经接触到“缓冲区”这个东西&#xff0c;但是相信大家在C语言阶段并没有真正弄懂缓冲区到底是个什么东西&#xff0c;也相信大家在C语言阶段也因为缓冲区的问题写出过各种bug。 其…

【计算机视觉】万字长文详解:卷积神经网络

以下部分文字资料整合于网络&#xff0c;本文仅供自己学习用&#xff01; 一、计算机视觉概述 如果输入层和隐藏层和之前一样都是采用全连接网络&#xff0c;参数过多会导致过拟合问题&#xff0c;其次这么多的参数存储下来对计算机的内存要求也是很高的 解决这一问题&#x…

2024.2.4

双向链表的头插 头删 尾插 尾删 //头插插入 Doublelink insert_head(Doublelink head,datatype element) {Doublelink screat_Node();s->dataelement;//判断是否有空链表if(NULLhead){heads;}else{s->nexthead;head->priors;heads;}return head; } //头删 Doublelink…

sql相关子查询

1.什么是相关子查询 相关子查询是一个嵌套在外部查询中的查询&#xff0c;它使用了外部查询的某些值。每当外部查询处理一行数据时&#xff0c;相关子查询就会针对那行数据执行一次&#xff0c;因此它的结果可以依赖于外部查询中正在处理的行。 2.为什么要使用相关子…

微信小程序之本地生活案例的实现

学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持&#xff0c;想组团高效学习… 想写博客但无从下手&#xff0c;急需…

图论练习3

内容&#xff1a;过程中视条件改变边权&#xff0c;利用树状数组区间加处理 卯酉东海道 题目链接 题目大意 个点&#xff0c;条有向边&#xff0c;每条边有颜色和费用总共有种颜色若当前颜色与要走的边颜色相同&#xff0c;则花费为若当前颜色与要走的边颜色不同&#xff0c;…

Android学习之路(27) ProGuard,混淆,R8优化

前言 使用java编写的源代码编译后生成了对于的class文件&#xff0c;但是class文件是一个非常标准的文件&#xff0c;市面上很多软件都可以对class文件进行反编译&#xff0c;为了我们app的安全性&#xff0c;就需要使用到Android代码混淆这一功能。 针对 Java 的混淆&#x…

【快速上手QT】01-QWidgetQMainWindow QT中的窗口

总所周知&#xff0c;QT是一个跨平台的C图形用户界面应用程序开发框架。它既可以开发GUI程序&#xff0c;也可用于开发非GUI程序&#xff0c;当然我们用到QT就是要做GUI的&#xff0c;所以我们快速上手QT的第一篇博文就讲QT的界面窗口。 我用的IDE是VS2019&#xff0c;使用QTc…

Leetcode高频题:213打家劫舍II

题目链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目描述 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋&#xff0c;每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 &#xff0c;这意味着第一个房屋和最后一个…

MySQL知识点总结(三)——事务

MySQL知识点总结&#xff08;三&#xff09;——事务 事务事务的四大特性ACID原子性一致性隔离性持久性 脏读、幻读、不可重复读脏读不可重复读幻读 隔离级别读未提交读已提交可重复读串行化 事务的原理InnoDB如何实现事务的ACID事务的两阶段提交redo log与binlog的区别事务两阶…

基于SpringBoot+Vue的在线教育平台设计与实现

目录 项目介绍 技术栈 项目介绍 项目截图 搭建 代码截取 代码获取 项目介绍 近年由于疫情影响&#xff0c;线下教育行业受到较大冲击&#xff0c;因此线上教育培训有较好的发展势头&#xff0c;其中建筑行业考证培训是一个前景良好的发展方向&#xff0c;该行业不仅需要…

权威认可|亚数强势入围FreeBuf《CCSIP 2023中国网络安全产业全景图》10大细分领域

近日&#xff0c;国内安全行业门户FreeBuf旗下FreeBuf咨询正式发布《CCSIP&#xff08;China Cyber Security Industry Panorama&#xff09;2023中国网络安全行业全景册&#xff08;第六版&#xff09;》。 凭借卓越的技术产品能力、市场影响力及领先的综合实力&#xff0c;亚…

C++泛编程(3)

类模板基础 1.类模板的基本概念2.类模板的分文件编写3.类模板的嵌套 &#xff08;未完待续...&#xff09; 在往节内容中&#xff0c;我们详细介绍了函数模板&#xff0c;这节开始我们就来聊一聊类模板。C中&#xff0c;类的细节远比函数多&#xff0c;所以这个专题也会更复杂。…

【Crypto | CTF】BUUCTF 萌萌哒的八戒

天命&#xff1a;这年头连猪都有密码&#xff0c;真是奇葩&#xff0c;怪不得我一点头绪都没有 拿到软件&#xff0c;发现是.zip的压缩包&#xff0c;打不开&#xff0c;改成7z后缀名&#xff0c;打开了 发现是一张图片 也只有下面这行东西是感觉是密码了&#xff0c;又不可能…

[leetcode] 22. 括号生成

文章目录 题目描述解题方法方法一&#xff1a;dfs遍历java代码 方法二&#xff1a;按照卡特兰数的思路递归求出有效括号组合java代码 相似题目 题目描述 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示…