用了这么久了,我竟然还不知道它是怎么跑起来的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
如何打包:
- gcc得到.o文件,即和位置无关的代码
gcc -c -fpic/-fPIC a.c b.c - gcc 得到动态库
gcc -shared a.o b.o -o libxxx.so
从而获得libxxx.so文件
如何使用自定义动态库
参照自定义静态库完成编译链接
动态库和静态库的区别是什么?
静态库:代码会被打包到可执行程序中
动态库:代码不会被打包到可执行程序中
因此,如果一个程序使用了动态库,在其启动的时候,动态库会被动态加载到内存之中,可以通过”ldd [file]”命令检查动态库依赖关系如何定位动态库?
使用系统的动态载入器来获取绝对路径现在主流的linux动态库加载器为ld-linux.so
如何将自己的动态库添加到加载器内?
- 会话级配置:
直接执行export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[new path]
解释:在LD_LIBRARY_PATH内追加新动态库路径,$[]为对原有系统变量的引用
缺点:在终端关闭后即失效 - 用户级配置:
在~/.bachrc文件中加入export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[new path]
加入后使用source .bachrc进行更新 - 系统级配置
在/etc/profile中加入export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[new path]
加入后使用source /etc/profile进行更新(不需要sudo)
动态库和静态库的优缺点
静态库
优点:
- 静态库被打包进程序,加载速度较快
- 发布时直接发布程序即可,移植工作量小
缺点: - 占用空间较大
- 更新、部署、发布比较麻烦,更改其中一个文件需要重新发布整个程序
动态库
优点:
- 可以实现进程间资源共享
- 更新、部署、发布简单
- 可以控制加载时间,使用到再加载
缺点: - 相对于静态库,加载速度慢
- 发布程序时需要提供其依赖的动态库
Makefile
作用:对于一个大的项目来说,他们的依赖关系非常复杂,如果使用单挑命令进行编译,时候维护难度会相对较大,使用makefile文件,可以指定文件的编译顺序、编译选项,同时,还可以实现自动化编译,在对代码进行更改后执行make命令就可以完成编译
文件名:makefile/Makefile
规则
- 一个makefile文件中可以有一个或者多个规则
目标…:依赖…
  命令….(注意制表符)
目标:最终需要生成的文件
依赖:生成目标所需要的文件或目标
命令:通过执行命令对依赖操作生成目标(必须有缩进) - 一般来说,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 |
|
read&write
1 |
|
lseek函数
1 |
|
stat/lstat
1 |
|
文件属性操作函数
1 |
|
目录操作函数
1 |
|
目录遍历函数
1 |
|
关于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 |
|