ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

C语言 ---- 第四章 函数库

2020-01-21 18:05:23  阅读:240  来源: 互联网

标签:服务 函数 程序 C语言 POSIX 函数库 第四章 操作系统


第一节 系统服务

  • 写一个程序,实现在屏幕上显示一串字符的功能,从代码开始执行到字符显示在屏幕上,这中间都发生了什么?
  • 补充知识:I/O设备
    • I/O设备分为三部分:设备、设备控制器、设备驱动程序
      • 设备:显示器、键盘、鼠标、硬盘、打印机、麦克风、···
      • 设备控制器:显卡、声卡、网卡、硬盘控制器、···
      • 设备驱动程序:每一种设备都有相应的设备驱动程序
    • 要想在屏幕上显示一个字符,显示器显卡及其相应的设备驱动程序,三者缺一不可
  • 从代码开始执行到字符显示在屏幕上,这中间都发生了什么?显示器、显卡及其相应的设备驱动程序,它们分别处于整个显示过程的什么位置?
    • 程序向操作系统发送服务请求
    • 操作系统把服务请求翻译给设备驱动程序
    • 设备驱动程序根据服务请求与设备控制器进行交涉
    • 设备控制器最终控制设备完成我们所请求的服务
  • 在屏幕上显示一个字符听起来很简单,但具体实现过程相当复杂,但是,在整个实现的过程中,和程序有直接关系的只有操作系统。程序只要向操作系统发送服务请求即可,剩下的事情就和程序没有关系了,就好像是操作系统屏蔽了硬件的所有细节,我们不需要了解硬件的任何相关信息,操作系统背后的东西与我们无关。
  • 操作系统为程序提供了在屏幕上输出字符的服务,对于我们来说,只需要按照操作系统所规定的方法向操作系统发送服务请求,就可以实现这个功能。除此之外,操作系统还提供了其他服务,将操作系统提供的这些服务简称为系统服务

第二节 系统调用

  • 程序具体应该如何向操作系统请求系统服务?

    • 我们需要按照操作系统所规定的方法来请求系统服务,不同的操作系统对应着不同的请求方法,但是它们的原理是相同的。

    • 以Linux操作系统为例,探究在Linux操作系统中如何请求系统服务:

      • C语言并不具备请求系统服务的能力,需要通过在C程序中嵌入汇编代码来实现系统服务的请求。

      • int main(){
            char A[] = {"Hello\n"};
            __asm__(
            	"mov rax, 1 \n\t"
                "mov rdi, 1 \n\t"
                "syscall \n\t"
                :
                :"S"(A),"d"(6)
            );
            
            return 0;
        }
      • __asm__();是汇编代码的一个标识,用来区分汇编代码和C代码,如果不按照这个格式写,C实现就无法判断这段代码是汇编代码还是C代码,就会按照C语言的语法对代码进行解析,这就必然会导致编译失败。

      • " \n\t":和C语言中的;作用相同,用于代码和代码之间的区分,表示mov rax, 1是一条完整的代码。

      • "mov rax, 1 \n\t":rax寄存器中存储的值为系统服务号,将rax寄存器的值设置为1,也就是要求rax寄存器提供1号系统服务,即写文件和设备服务。

      • "mov rdi, 1 \n\t":rdi寄存器决定写哪个文件和设备,将rdi寄存器的值设置为1,也就是要求rdi寄存器写控制台。

      • "syscall \n\t":向操作系统请求系统服务,操作系统会根据上述两行代码的要求来提供相应的系统服务。

      • "S"(A):S表示rsi寄存器,用于存储要显示字符(A)的内存地址

      • "d"(6):d表示rdx寄存器,用于存储要显示的字符(“Hello\n”)数量

      • ::寄存器和C语言的变量进行交换数据的指令前面要加上冒号,以示和其他指令的区别

  • 总结:

    • 操作系统为我们提供了很多服务(比如在控制台显示字符的服务),我们把操作系统提供的服务简称为“系统服务”。
    • 操作系统为我们提供了调用系统服务的“入口点”,即syscall,我们可以通过syscall这个入口点来调用系统服务,把用来调用系统服务的入口点简称为“系统调用”。(syscall 就是 system call的简写)
    • 通过系统调用获取系统服务,需要用汇编代码来实现。
  • 根据以上总结,可以得出以下结论:

    • C语言并不具备在屏幕上显示字符的能力,显示字符是一件很复杂的事情;
    • C语言只是显示字符这个行为的触发者,只是在代码中通过系统调用触发了显示字符的这个行为,而显示字符这个行为的具体实现,是由操作系统及其后面的一系列设备共同来完成的。
  • 另外,C语言也不具备通过系统调用获取系统服务的能力,通过系统调用获取系统服务是由汇编代码来实现的,严格来讲,C语言只是作为汇编代码的载体,只能勉强算是行为的触发者。

第三节 将系统调用封装成函数

void write(char * A){//参数类型为指向char类型的指针
    int B = 0;
    for(char * C = A; C ++ != '\0'; B++);
     __asm__(
    	"mov rax, 1 \n\t"
        "mov rdi, 1 \n\t"
        "syscall \n\t"
        :
        :"S"(A),"d"(B)
    );
    
}

int main(){
    write("123\n");
    write("456\n");
    write("789\n");
    
    return 0;
}
  • B记录字符串中字符的数量(4个),字符的数量并不包括最后的空字符。
  • "S"(A):S表示rsi寄存器,用于存储要显示字符(A)的内存地址(首地址)。
  • "d"(B):d表示rdx寄存器,用于存储要显示的字符(B)数量。
  • for循环的作用:通过变量B来记录要显示的字符的数量。
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bkqwmY3q-1579599880107)(E:\workspace\TyporaProjects\C笔记\C语言\images\第四章 函数库\3.png)]
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8JVBrcvo-1579599880108)(E:\workspace\TyporaProjects\C笔记\C语言\images\第四章 函数库\3-2.png)]

第四节 多个源文件组成的程序、翻译和链接

  • fun.c

  • void write(char * A){//参数类型为指向char类型的指针
        int B = 0;
        for(char * C = A; C ++ != '\0'; B++);
         __asm__(
        	"mov rax, 1 \n\t"
            "mov rdi, 1 \n\t"
            "syscall \n\t"
            :
            :"S"(A),"d"(B)
        );
        
    }
  • main.c

  • void write(* char);
    int main(){
        write("123456789\n");
        return 0;
    }
  • 在main.c程序的开头添加了程序的原型void write(* char);,故即使没有函数的主体,在编译时也不会产生语法错误。

  • 怎样做才能使程序在屏幕上打印出字符串?

    • 1.将fun.c程序复制到main.c程序中。
      - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZCDdY1M-1579599880109)(E:\workspace\TyporaProjects\C笔记\C语言\images\第四章 函数库\4-1.png)]
    • 将main.c程序和fun.c函数同时进行编译。
      - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TBpV8cJP-1579599880110)(E:\workspace\TyporaProjects\C笔记\C语言\images\第四章 函数库\4-2.png)]
  • 源文件(.c)通过编译(gcc)生成可执行文件(.out)

    • 编译的过程分为两步:对源文件进行翻译,翻译以后得到一个目标文件,再对目标文件进行一个链接。
    • 也就是:源文件(.c) ----> 翻译(gcc -c) ----> 目标文件(.o) ----> 链接(gcc) ----> 可执行文件(.out)
      • 翻译:简单的来说,就是对源代码进行语法检查,将源代码翻译为机器指令(翻译成中间文件)。
      • 链接:将两个源文件所对应的目标文件进行链接,生成一个可执行文件。
  • 在命令行输入:

    • gcc -c fun.c -masm=intel回车,生成一个fun.o文件
    • gcc -c main.c回车,生成一个main.o文件
    • gcc main.o fun.o回车,生成一个a.out文件
    • gcc fun.o main.c回车,仍然生成一个a.out文件

第五节 函数库

  • 当一个功能非常重要时,一般会将其封装为函数,进行单独存储,并生成其对应的目标(中间)文件来方便在其他程序中对其进行调用。
  • 假设目前有fun1.c、fun2.c、fun3.c、main.c四个源程序,其中fun1.c源程序中有汇编代码,main.c程序分别调用了fun1.c、fun2.c、fun3.c:
    • gcc -c fun1.c fun2.c fun3.c -masm=intel回车,分别生成三个相对应的目标(中间)文件fun1.o、fun2.o、fun3.o
    • gcc -c main.c fun1.o fun2.o fun3.o回车,生成一个可执行文件a.out
  • 如果文件越来越多,对其进行逐个编译就变得异常麻烦,使用ar r libxxx.a fun1.0 fun2.o fun3.o命令,将fun1.0 fun2.o fun3.o三个文件都存储到libxxx.a文件中,在编译时就可以更加方便的进行gcc main.c libxxx.a回车,生成一个可执行文件a.out,其中,以.a结尾的文件称之为库文件,用来存储函数,故函数库由此诞生,库函数则为函数库中的函数。
  • 注意:
    • 在给库文件或者说函数库起名字的时候,以lib开头。
    • 使用ar r libxxx.a fun1.0 fun2.o fun3.o命令生成库文件,当生成的库文件名已存在时,该命令会将目标文件添加到已存在的库文件中,如果没有以命令中lib开头命名的库文件,则会自动创建一个库文件,并将目标文件添加到库文件中。
    • 若库文件中已经存在命令中的目标文件,新的目标文件就会将库文件中已有的目标文件(同名的目标文件)覆盖掉。

第六节 静态库、共享库

  • 函数库分为两种:静态库(.a)和共享库(.so)。
  • 静态库(.a)
    • 如何生成静态库?
      • arr r libxxx.a fun1.o fun2.o fun3.o
    • 源文件和静态库如何进行链接生成可执行程序?
      • gcc main.c libxxx.a
    • 源文件和静态库链接生成的可执行程序中,包含它所用到的函数的代码,程序体积相对较大。(注意:可执行程序中只载入了它需要的函数,而不是载入整个静态库中的函数)
  • 共享库(.so)
    • 如何生成共享库?
      • gcc -shared -o libxxx.so fun1.o fun2.o fun3.o
    • 源文件和共享库如何进行链接生成可执行程序?
      • gcc main.c libxxx.so
    • 源文件和共享链接生成的可执行程序中不包含它所用到的函数的代码,程序体积相对较小。可以简单的理解为包含了共享库的一个引用。
    • 当可执行程序需要调用函数时怎么办?
      • 程序启动以后,当执行到共享库中的函数时,程序会找到共享库所在的位置,然后将其所需要的函数从共享库加载到内存中。
    • 程序到哪里找到共享库?
      • export_LD_LIBRARY_PATH = ./
    • 为什么叫做共享库?
      • 假设,有5个程序用到了共享库中的函数A,并且这5个程序同时在运行,函数A只会被加载1次,这5个程序将共享被加载到内存中的同一个函数A。
      • 优点:体积小,不会造成内存浪费、更新/升级程序更加方便,只需要更新共享库即可,不需要改动程序本身。

第七节 头文件、预处理指令、文件包含指令

  • 当我们需要大量的,频繁的使用函数库时,在程序的开头声明函数原型就变成了一件非常麻烦的事情,那么该如何弥补这个缺陷,使编程更加轻松?
    • 第一步:可以把一个函数库中所有函数原型的声明集中到一起,保存为一个以.h结尾的文件,这个文件称之为头文件。
      • 打开编辑器,将函数的原型写进去,保存为.h即可。
    • 第二步:#include "xxx.h"
      • #是预处理指令的标识
      • 预处理指令有很多种,include是其中之一,文件中包含指令
      • 作用:将它自己替换为它所指定的文件中的内容。
    • 程序的编译分为三步:
      • 预处理 ----> 翻译 -----> 链接
        • 预处理:根据指令修改源文件
        • 函数库
        • 头文件和文件包含指令相互配合可以使得程序的编写更加轻松。

第八节 API

  • 操作系统给我们提供了很多服务,比如在控制台显示字符串的服务,我们把操作系统提供的这些服务称为系统服务,有了操作系统提供的服务以后,只需要向操作系统发送服务请求,即可实现很强的功能,再换句话说就是系统服务的存在,就是为了方便程序的开发。
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vbgVEpvj-1579599880110)(E:\workspace\TyporaProjects\C笔记\C语言\images\第四章 函数库\8-1.png)]
  • 如何向操作系统请求系统服务?
    • 操作系统提供了获取系统服务的入口点,程序可以通过入口点向操作系统请求服务(比如linux中的syscall),用来获取系统服务的入口点,称之为“系统调用”。毕竟程序必须要通过它才能调用系统服务。
      • 通过系统调用获取系统服务的这个动作,由于需要用汇编代码实现,所以非常的繁琐,故需要简化操作,让程序的开发更方便。
    • 我们可以将发送服务请求的汇编代码嵌入C程序中,并将其封装为函数。
      • 一个系统服务就对应着一个函数,当需要调用系统服务时,就不需要单独编写系统调用的代码,直接调用对应的函数即可。
    • 当程序中使用了大量的函数时,函数原型的声明也将成为一个编程负担。可以将这些函数封装到库文件里面,形成一个函数库,并创建一个和库文件相对应的头文件,用于存储函数的原型声明,有了库文件和头文件以后,就可以将程序中大量的函数原型的声明替换为一条文件包含指令。
    • 操作系统所提供的服务非常强大,程序会大量使用它们,为了方便,一般只会通过库函数来使用它们,而这些函数就像是程序通往操作系统的一个入口(接口)。
    • 编写应用程序就避免不了要通过库函数调用系统服务,所以我们也把库函数称为应用程序编程接口,即API
    • 并不是所有的库函数都是用来封装系统调用的,有些库函数封装了系统调用,用于调用系统服务,比如在控制台显示字符的函数;有些库函数则没有封装系统调用,比如计算字符串长度的函数,比较两个数大小的函数,这些函数使用C语言给我们提供的各种运算符和语句就可以实现,所以并不是所有的库函数都是用来封装系统调用的。

第九节 POSIX(Unix、Linux)

  • C语言的诞生是为了重写UNIX操作系统,UNIX操作系统又是C语言程序设计的重要平台,所以,为了在UNIX平台上使用C语言进行编程更加的方便,快捷,UNIX操作系统中的系统调用,必然会被封装为C函数,并进一步的被封装为C库,以方便程序的开发。
  • 随着UNIX操纵系统的不断发展,其版本就越来越多,版本之间的差异也就越来越大,比如,UNIX-A中有一个系统服务A,而UNIX-B中可能就没有这个服务。它们的系统服务存在差异,则其函数库也必然存在差异,而各个版本之间的差异,严重影响了程序的可移植性。
  • 在这里插入图片描述
    • 假设我们在UNIX-A上写了一个程序,程序中使用了一个库函数,这个库函数就调用了系统服务A,那么这个程序就只能在UNIX-A中运行,而无法在UNIX-B中运行(原因:UNIX-B里面压根就没有这个函数,也没有这个服务,所以程序也就不具备可移植性)。
  • 后来,IEEE协会制定了一个标准 – POSIX标准(可移植性操作系统接口)。
  • POSIX标准最初的目的是提升应用程序在各种UNIX操作系统之间的可移植性。POSIX定义了操作系统必须提供的系统服务和一个函数库(并以头文件的形式发布),也就是说,它把系统服务和函数库都统一了起来,简单来说就是把接口统一了起来。
  • POSIX含义解析(以头文件的形式发布是什么意思)
    • 只提供了头文件,并没有提供库文件,也就是只定义了函数的原型。只是规定了有多少个函数,函数的名字是什么,函数的参数是什么,函数的返回值是什么,函数实现了什么功能(函数具体是如何实现的并不进行深究,但是必须要有这个函数,要实现这个功能)。
    • 库函数是由谁来提供的?
      • 库函数是由编译器厂商来提供的,编译器厂商会根据编译器所运行平台的具体情况来实现POSIX所定义的函数,然后,编译器厂商会把库文件、头文件和编译器打包在一起,故而我们在下载编译器的时候,里面就包含了库文件和头文件。每一种编译器都会提供一些流行的函数库。
    • 正是由于上述原因,我们在UNIX操作系统上进行编程的时候,就可以使用POSIX所定义的函数,这样,程序就可以在UNIX操作系统之间进行移植。
    • POSIX只是定义了函数原型,并没有定义函数体,也没有统一函数的具体实现,这样会造成什么影响?
      • 没有任何影响。
    • LINUX操作系统也支持POSIX标准,所以,编写运行在UNIX或者LINUX平台上的程序时,可以使用POSIX所定义的函数。

第十节 POSIX标准库到底是由谁来提供的

  • POSIX标准是给操作系统制定的,规定了操作系统必须提供的系统调用和函数库,需要注意的是,函数库是通过头文件来规定的。
  • 都有哪些操作系统支持POSIX标准?
    • UNIX和LINUX都支持POSIX标准,所以,UNIX和LINUX都必须按照该标准的要求,提供相应的系统调用及其函数库,而所谓的提供函数库,指的就是提供库文件及其相应的头文件。
  • POSIX标准是给操作系统制定的,如果某个操作系统支持这个标准,这个操作系统就必须按照标准的要求来提供相应的服务。POSIX标准分别对系统调用和函数库进行了规定,必须按照要求来提供相应的系统调用和库文件。所以我们说UNIX和LINUX都按照标准的要求,提供了相应的系统调用及其函数库。
  • 操作系统提供的函数库(头文件、库文件)和编译器所提供的函数库(头文件、库文件)之间,有什么区别
    • 二者提供的函数库本身并没有任何区别;
    • 操作系统支持POSIX标准,所以必须提供头文件和库文件。而编译器提供或者不提供都是可以的,没有一个强制的要求,只不过大多数的编译器都会提供一些比较流行的函数库给用户使用。

第十一节 动态链接库(windows)

  • windows中的函数库
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dl8cbNRR-1579599880111)(E:\workspace\TyporaProjects\C笔记\C语言\images\第四章 函数库\11-1.png)]
    • windows是微软公司的私人产品,和其他的操作系统一样,为了能够更方便的在windows上开发程序,微软公司把windows的系统调用封装成了函数,并进一步封装成了函数库提供给开发者使用,需要注意的是,windows中的函数库并不是以.a或者.so结尾的,而是以.dll结尾的,windows将其称为动态链接库(Dynamic Linkable Library),动态链接库的性质和LINUX中的共享库的性质是一致的,都是在程序运行时进行链接的,显而易见,开发基于LINUX操作系统的程序,使用POSIX标准库将是最好的选择,使用POSIX标准库更利于程序在LINUX操作系统之间进行移植;开发基于windows操作系统的程序,使用windows提供的动态链接库将是最好的选择,由于windows是微软公司私有的,只有一个系列,并没有其他的分支,所以在windows中并不存在程序移植的问题。

第十二节 C语言标准的发展脉络(编译器)

  • 1973年,C语言诞生,没有标准

  • 1978年,《C程序设计语言》出版,K&R C / 经典C

  • 1989年,ANSIC(C89),定义了语言本身,定义了函数库(C标准库)

  • 1990年,ISO C(C90),ISO C == ANSI C

  • 1999年,ISO C(C99)

  • 2011年,ISO C(C11)

  • UNIX/LINUX提供了POSIX标准库。

  • windows提供了动态链接库。

  • 其他操作系统提供了其他相应的函数库来供开发者使用。

  • 如果程序中使用了动态链接库,则只能运行在windows上,如果程序中使用了POSIX标准库,则只能运行在UNIX/LINUX上,如果程序使用了其他函数库,则只能运行在函数库所对应的操作系统上。从当前阶段来看,函数库与操作系统是相关联的,C语言不具备可移植性。

  • C89、C90、C99、C11等标准是为C语言制定的,C89、C90、C99、C11等标准定义的函数库称为C标准库,而POSIX标准是为操作系统制定的,POSIX标准定义的函数库称为POSIX标准库。POSIX标准库是由操作系统提供的,C标准库与操作系统无关,在实际应用中,C标准库是由编译器提供的。
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IIT1a344-1579599880111)(E:\workspace\TyporaProjects\C笔记\C语言\images\第四章 函数库\12-1.png)]
    总结:

    • 不同点:POSIX标准库是由操作系统提供的,C标准库是由编译器提供的。
    • 相同点:函数库的定义都是通过头文件(.h)来实现的。
  • 如何通过C标准库来实现可移植性?

    • 基本上所有的编译器都支持C标准库,运行在windows上的编译器支持C标准库,运行在UNIX/LINUX上的编译器支持C标准库,运行在其他操作系统上的编译器也支持C标准库,所以,如果在程序中使用C标准库,将程序移植到任何操作系统上,都可以成功运行。
    • 简单来说,C语言的可移植性是通过编译器对C标准库的支持来实现的。
、南、 发布了11 篇原创文章 · 获赞 3 · 访问量 861 私信 关注

标签:服务,函数,程序,C语言,POSIX,函数库,第四章,操作系统
来源: https://blog.csdn.net/Insist0224/article/details/104064296

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有