1. 1. gcc的常用选项
  2. 2. gcc工作流程
  • 静态库和动态库
    1. 1. 静态库
      1. 1.0.1. 命名规则
      2. 1.0.2. 如何打包
      3. 1.0.3. 如何使用自定义静态库
  • 2. 动态库
    1. 2.0.1. 命名规则
    2. 2.0.2. 如何打包:
    3. 2.0.3. 如何使用自定义动态库
    4. 2.0.4. 动态库和静态库的优缺点
      1. 2.0.4.1. 静态库
      2. 2.0.4.2. 动态库
  • Makefile
    1. 1. 规则
    2. 2. 工作原理:
    3. 3. 变量
      1. 3.0.1. 自定义变量
      2. 3.0.2. 预定义变量
      3. 3.0.3. 获取变量的值
  • 4. 函数
  • GDB
    1. 1. 准备工作
    2. 2. 开始工作
      1. 2.0.1. GDB常用命令
      2. 2.0.2. gdb调试命令
  • 文件IO
    1. 1. C语言中的文件IO:
    2. 2. 虚拟地址空间
      1. 2.0.1. 用户区
      2. 2.0.2. 内核区
  • 文件描述符与系统API
    1. 0.0.1. open&close
    2. 0.0.2. read&write
    3. 0.0.3. lseek函数
    4. 0.0.4. stat/lstat
    5. 0.0.5. 文件属性操作函数
    6. 0.0.6. 目录操作函数
    7. 0.0.7. 目录遍历函数
    8. 0.0.8. 文件描述符相关函数
  • 用了这么久了,我竟然还不知道它是怎么跑起来的QAQ

    gcc的常用选项

    选项 作用 备注
    -E 预处理指定文件
    -S 编译指定的源文件
    -c 编译、汇编但不链接
    -o [file1] [file2] 将file2编译成可执行文件file1
    -I 指定include的搜索目录
    -g 编译时生成调试信息
    -D 程序编译的时候指定一个宏
    -w 不生成任何警告信息
    -Wall 生成所有警告信息
    -On n=[0,3] 优化级别
    -l 指定编译时使用的库
    -L 指定编译的时候搜索的库路径
    -fPIC/fpic 生成与位置无关的代码
    -shared 生成共享目标文件
    -std 指定语言标准 如-std=c99

    gcc工作流程

    原代码(.h/.c/.cpp)

    –预处理器–>

    预处理后源代码

    –编译器–>

    汇编代码(.s)

    –汇编器–>

    (启动代码、目标代码、库代码、其他目标代码)

    –链接器–>

    可执行程序

    静态库和动态库

    静态库

    • 注意:库文件需要和对应的头文件一起使用

    命名规则

    • linux平台
      前缀:lib
      后缀:.a
    • windows平台
      前缀:lib
      后缀:.lib

    如何打包

    将写好的源码通过gcc获得.o文件
    使用ar工具插入 ar rcs libxxx.a xxx.o,xxx.o(需要被打包的a文件,可以是多个)
    r:将文件插入备存文件
    c:简历备存文件
    s:索引
    获得libxxx.a文件

    如何使用自定义静态库

    答:在编译时指定扫描路径
    gcc xxx.c -o app -I ../include/ -l calc -L ./lib
    小小的回忆一下之前的知识:
    -I 指定include的搜索目录
    -l 指定编译的时候使用的库
    -L 指定编译的时候搜索的库的路径
    linux严格区分大小写

    动态库

    命名规则

    • linux平台
      前缀:lib
      后缀:.so
    • windows平台
      前缀:lib
      后缀:.dll

    如何打包:

    1. gcc得到.o文件,即和位置无关的代码
      gcc -c -fpic/-fPIC a.c b.c
    2. gcc 得到动态库
      gcc -shared a.o b.o -o libxxx.so
      从而获得libxxx.so文件

    如何使用自定义动态库

    参照自定义静态库完成编译链接

    • 动态库和静态库的区别是什么?
      静态库:代码会被打包到可执行程序中
      动态库:代码不会被打包到可执行程序中
      因此,如果一个程序使用了动态库,在其启动的时候,动态库会被动态加载到内存之中,可以通过”ldd [file]”命令检查动态库依赖关系


    • 如何定位动态库?
      使用系统的动态载入器来获取绝对路径
      现在主流的linux动态库加载器为ld-linux.so


    • 如何将自己的动态库添加到加载器内?

    1. 会话级配置:
      直接执行export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[new path]
      解释:在LD_LIBRARY_PATH内追加新动态库路径,$[]为对原有系统变量的引用
      缺点:在终端关闭后即失效
    2. 用户级配置:
      在~/.bachrc文件中加入export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[new path]
      加入后使用source .bachrc进行更新
    3. 系统级配置
      在/etc/profile中加入export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[new path]
      加入后使用source /etc/profile进行更新(不需要sudo)

    动态库和静态库的优缺点

    静态库

    优点:

    1. 静态库被打包进程序,加载速度较快
    2. 发布时直接发布程序即可,移植工作量小
      缺点:
    3. 占用空间较大
    4. 更新、部署、发布比较麻烦,更改其中一个文件需要重新发布整个程序
    动态库

    优点:

    1. 可以实现进程间资源共享
    2. 更新、部署、发布简单
    3. 可以控制加载时间,使用到再加载
      缺点:
    4. 相对于静态库,加载速度慢
    5. 发布程序时需要提供其依赖的动态库

    Makefile

    作用:对于一个大的项目来说,他们的依赖关系非常复杂,如果使用单挑命令进行编译,时候维护难度会相对较大,使用makefile文件,可以指定文件的编译顺序、编译选项,同时,还可以实现自动化编译,在对代码进行更改后执行make命令就可以完成编译
    文件名:makefile/Makefile

    规则

    • 一个makefile文件中可以有一个或者多个规则
      目标…:依赖…
      &nbsp&nbsp命令….(注意制表符)
      目标:最终需要生成的文件
      依赖:生成目标所需要的文件或目标
      命令:通过执行命令对依赖操作生成目标(必须有缩进)
    • 一般来说,makefile的其他规则都是为了第一条规则服务的

    工作原理:

    • 命令在执行前,会检查规则中的依赖是否存在
      • 如果存在,则执行
      • 如果不存在,会向下检查其他规则,如果找到了,执行
    • 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
      • 如果依赖的(最后修改)时间比目标的时间晚,则需要重新生成目标
      • 如果依赖的时间比目标的时间早,对应规则的命令不会被执行

    变量

    自定义变量

    [name]=[value]

    预定义变量

    AR 归档维护程序的名称,默认为ar
    CC c的编译器的名称,默认为gcc
    cxx c++的编译器的名称
    $@ 目标的完整名称
    $< 第一个依赖文件的名称
    $^ 所有的依赖文件
    …….
    还有很多,这里只列举常用的

    获取变量的值

    $(变量名)

    函数

    $(函数名 参数)
    比如:
    $(wildcatd PATTERN)…
    功能:获取指定目录下指定类型的文件列表
    参数:pattern指的是某个或多个目录下某种类型的文件
    返回值:得到一个文件列表,文件名之间用空格间隔
    示例:
    $(wildcard .c ./sub/.c)
    获取当前目录与同级sub目录下的所有*.c文件名
    $(patsubst,<pattern>,<replacement>,<text>)
    查找<text>中的单词是否符合<pattern>模式,如果匹配的话,用<replacement>替换
    示例:
    $(patsubst %.c,%.o,x.c bar.c)
    返回值格式:x.o bar.o

    GDB

    gdb是由gnu组织提供的调试工具,和gcc配套组成了一套完整的开发环境
    主要协助完成如下功能:

    • 按照自定义的要求运行程序
    • 在指定的断电处暂停程序运行
    • 在程序暂停运行时检查内部运行状态
    • 可以改正程序

    准备工作

    • 编译时加上调试选项’-g’
      调试选项的作用:在可执行文件中加入源代码的信息,使可执行文件的指令和源代码的行数建立联系,但并不嵌入源文件,因此必须保证gdb可以找到源文件
    • 编译时加上’-Wall’ 打开所有warning
    • 关闭编译器优化(-o)

    开始工作

    使用命令gdb进入gdb环境

    GDB常用命令

    • 启动和退出
      gdb [name]
      quit
    • 设置属性并获取
      set [name] [value]
      show [name]
    • 查看当前文件代码
      list/l [文件名]:[行号/函数名]
      若留空文件名,只查看当前文件
      使用show list/listsize 可以查看当前一次展示的行数
      使用set list/listsize [number]可以修改每次显示的行数(defaule 10)
    • 设置断点
      break/b [文件名]:[行号/函数名]
      这样设置好的是普通断点
      break/b [文件名]:[行号/函数名] [条件]
      这样可以在某一行设置条件断点(一般用于循环)
      例: b test.cpp:9 if i==3
    • 查看断点
      info/i break/b
      (是一个整体,比如info break这样输入)
    • 删除断点
      d/del/delete [num]
      注意是断点编号不是断点所在行数,可以使用info查看断点编号
    • 设置断点有效/无效
      enable/disable [num]

    gdb调试命令

    • 运行程序
      start 开始运行,但停留在第一行
      run 开始运行,直到遇到可以停顿的断点
    • 继续运行,到下一个断点停
      continue/c
    • 向下执行一行代码(不会进入函数体)
      next/n
    • 操作变量
      print/p [name] 打印变量的值
      ptype [name] 打印变量的类型
    • 向下单步调试
      step/s 向下运行一行
      finish 跳出当前函数体
    • 自动变量操作
      display [name] 每次运行到断点处时自动打印指定变量的值
      undisplay [name]
      info/i [display/undisplay] [num]
    • 其他变量操作
      set var [name]=[value]
      until(跳出循环)
    • 小tip
      在断点所在行停留时,该行代码并不会被执行,继续运行代码后才会执行该行代码

    文件IO

    C语言中的文件IO:

    使用fopen或其他函数打开文件,返回值FILE *fp为一个指针,由三个部分组成

    • 文件描述符[整型值]: 索引对应的磁盘文件
    • 文件读写指针:读写文件过程中指针的实际位置(内存)
    • I/O缓存区[内存地址]:通过寻址找到对应的内存块(默认为8192byte)
      缓存区的意义:
      软件直接写入缓存区,当(
      1.缓存区达到最大容量时
      2.调用fflush
      3.正常关闭文件
      )时,缓冲区内容会被写入磁盘,可以提高系统运行效率
    • C语言IO和linux系统IO的关系
      程序<->C标准IO库(缓冲区在此)<–系统内核IO–>磁盘

    虚拟地址空间

    每个进程都会被分配一个虚拟地址空间,里面的数据会通过cpu的MMU(内存管理单元)映射到物理内存中,这段空间按照操作者被分为内核区和用户区

    用户区

    • 受保护的地址(0-4K)
    • .text 代码段
    • .data 已初始化的全局变量
    • .bss 未初始化的全局变量
    • 堆空间(较大)
    • 共享库(动态链接库)
    • 栈空间(较小)
    • 命令行参数 char* argv[]
    • 环境变量

    内核区

    即Linux kernel,不可以由程序进行操作,之可以通过程序调用系统API,从而间接更改内核数据

    • 内存管理
    • 进程管理
    • 设备驱动管理
    • VFS虚拟文件系统

    文件描述符与系统API

    open&close

    在上文提到的”内核区”中,有一个模块叫做PCB-进程控制块,指向文件描述符表
    每打开一个新文件,就占用空闲的最小的文件描述符
    系统IO函数和标准C库存在一定的对应关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    //运行在linux下
    int open(const char *pathname,int flags);

    /*
    pathname:需要打开的文件路径
    flags:对文件的操作权限设置/其他设置,可以是多个标记,多个标记之间使用|连接
    必选项:O_RDONLY/O_WRONLY/O_RDWR
    本质:32位的二进制数
    可选项:
    返回值:fd,文件描述符,若打开错误则返回-1
    */
    /*
    errno:属于linux函数库内的一个全局变量,记录的是最近的错误号
    #include<stdio.h>
    void perror(const char *s);
    //作用:打印errno对应的错误描述,打印格式:*s+"description"
    */
    int open(const char *pathname,int flags,mode_t mode);
    /*
    若读取文件不存在,可以创建新文件
    mode:新文件的操作权限
    类型/当前用户权限/当前用户所在组权限/其他组权限
    (用八进制的数表示,如:0777,前导零为八进制数的标志)
    最终的权限是:mode & ~umask
    rwx:每一位对应一个1(二进制),则最大数为7
    umask:每一个用户独有的值
    若mode=0777,umask=0002,则结果为0775
    意义:可以抹去某些特定的权限
    */
    int close(int fd);
    关闭fd所指向的连接

    read&write

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    //ssize_t:linux系统库<unistd.h>中用于计算字符数量的基类型
    ssize_t read(int fd, void *buf, size_t count);
    //fd:文件描述符,通过open得到
    //buf:被读取数据存放的数组地址
    //count:指定的数组大小,即缓冲区大小
    //返回值:读取到的字节数量,0代表EOF,-1代表读取失败
    ssize_t write(int fd, const void *buf, size_t count);
    //fd:文件描述符,通过open得到
    //buf:数据被写入的地址
    //count:要写的数据的实际大小
    //返回值:成功返回字节数,失败返回-1

    lseek函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<unistd.h>
    #include<stdio.h>
    off_t lseek(int fd,off_t offset,int whence);
    //fd:文件描述符
    //offset:偏移量
    //whence: 指定标记(SEEK_SET/SEEK_CUR/SEEK_END),设置偏移量为0+offser/设置为当前位置+offset/设置为文件大小+offset
    //返回值:最终所在的位置
    /*
    可以用的玩法:
    拓展文件的长度
    lseek(fd,100,SEEK_END)
    从文件末尾向后偏移100
    *** 最好在lseek之后再写入一些东西,不然可能拓展失败
    */

    stat/lstat

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<unistd.h>
    #include<stdio.h>
    int stat(const char *pathname, struct stat *statbuf)
    /*
    作用:获取文件信息
    参数:
    pathname:文件路径
    statbuff:结构体变量,传出参数,保存获取到的文件信息(你看他不是const!)
    返回值:
    获取成功返回0,失败返回-1并设置errno(314行)
    值得深究:st_mode
    */
    int lstat(const char *pathname, struct stat *statbuf)
    /*
    获取软链接信息,用stat会获取被指向文件的信息
    */

    文件属性操作函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include<unistd.h>
    int access(const char *pathname, int mode)
    /*
    判断某个文件是否有某个权限(当前进程)或某个文件是否存在
    -pathname 文件路径
    -mode 需要判断的文件权限
    格式:[F存在/R读/W写/X可执行]_OK
    返回值:存在返回0,失败返回-1
    */
    int chmod(const char *filename,mode_t mode)
    /*
    修改某个文件的某个权限
    -pathname 文件路径
    -mode 八进制数,表示权限
    (最后的权限是mode&mask)
    */
    int chown(const char *path,uid_t owner,gid_t gid)
    /*
    修改某文件的所有者为owner,所在组为gid
    */
    int truncate(const char *path, off_t length)
    /**
    缩减或扩展某文件大小
    注意:length是文件最终变成的大小
    */

    目录操作函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include<stdio.h>
    #include<unistd.h>
    int mkdir(const char *path,mode_t mode)
    /**
    创建一个目录
    mode:八进制数-访问权限
    返回值:成功返回0,失败返回-1
    */
    int rmdir(const char *path)
    /**
    删除一个目录
    */
    int rename(const char *oldpath,const char *newpath)
    /**将oldpath重命名为newpath
    */
    int chdir(const char *path)
    /** 修改当前进程的工作目录
    */
    char *getcwd(char* buf,size_t size)
    /** 获取当前进程的工作目录
    参数:
    -buf 当前目录的存储路径
    -size 数组的大小(buf的)
    返回值:成功返回*buf,失败返回NULL
    */

    目录遍历函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include<dirent.h>
    #include<sys/types.h>
    DIR *opendir(const char *name);
    /**打开一个目录,返回值为指向目录流的指针
    参数:需要打开的目录
    */
    struct dirent *readdir(DIR *dirp);
    /** 读取目录中的数据,指向目录中的下一个实体
    */
    int closedir(DIR *dirp);
    /**关闭文件流
    */

    关于dirent结构体:

    成员类型 成员名 说明
    ino_t d_ino 进入点的inode
    off_t d_off; 目录文件开头到本文件的位移
    unsigned short int d_reclen; d_name的长度
    usigned char d_type; 所指的文件类型
    char d_name[256]; 文件名

    文件描述符相关函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #include<fcntl.h>
    #include<unistd.h>
    int dup(int oldfd);
    /**复制新的文件描述符
    有点类似c++里的引用,复制出的文件描述符和原描述符(oldfd)指向同一个文件
    新fd由系统指定,一般为最小可用的文件描述符
    */
    int dup2(int oldfd,int newfd)
    /**拷贝(重定向)文件描述符
    新fd即传入的newfd,如果newfd已经被占用,则自动释放原占用并绑定到新的文件上
    因此有些文档里会写为"重定向文件描述符",将newfd重定向到oldfd上

    */
    int fcntl(int fd,int cmd,...)
    /**
    参数:
    fd:需要操作的文件描述符
    cmd:对文件描述符操作的命令(函数定义的宏)
    -F_DUPFD 复制文件描述符,得到一个新的文件描述符(由返回值返回)
    -F_GETFL 获取文件描述符的文件状态flag(可以参考open函数)
    -F_SETFL 设置文件描述符文件flag
    必选项:O_RDONLY/O_WRONLY/O_RDWR 不可以被更改
    可选项:O_APPEND(追加数据) O_NONBLOCK(设置为不阻塞模式)
    ...:可变参数
    返回值:
    */