Linux系统编程入门
约 4187 字大约 14 分钟
2025-03-26
GCC
(1)GCC工作流程

(2)GCC常用参数选项

gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
gcc hello.o -o hello
阶段 | 输入文件 | 输出文件 | 主要任务 |
---|---|---|---|
预处理 | hello.c | hello.i | 处理宏、头文件、条件编译,生成展开后的源代码。 |
编译 | hello.i | hello.s | 将C代码转换为汇编代码,进行语法和语义分析,优化代码。 |
汇编 | hello.s | hello.o | 将汇编代码转换为机器代码,生成目标文件。 |
链接 | hello.o | hello | 将目标文件与库文件合并,解析符号,生成可执行文件。 |
运行 | hello | 无 | 操作系统加载可执行文件并执行其中的机器指令。 |
(3)gcc与g++区别
gcc
和g++
都是GNU(组织)
的一个编译器- 误区一:
gcc
只能编译c
代码,g++
只能编译c++
代码- 后缀为
.c
的,gcc
把它当作是C
程序,而g++
当作是c++
程序 - 后缀为
.cpp
的,两者都会认为是C++
程序,C++
的语法规则更加严谨一些 - 编译阶段,
g++
会调用gcc
,对于C++
代码,两者是等价的,但是因为gcc
命令不能自动和C++
程序使用的库联接,所以通常用g++
来完成链接,为了统一起见,干脆编译/链接统统用g++
了,这就给人一种错觉,好像cpp
程序只能用g++
似的
- 后缀为
- 误区二:
gcc
不会定义__cplusplus
宏,而g++
会- 实际上,这个宏只是标志着编译器将会把代码按
C
还是C++
语法来解释 - 如上所述,如果后缀为
.c
,并且采用gcc
编译器,则该宏就是未定义的,否则,就是已定义
- 实际上,这个宏只是标志着编译器将会把代码按
- 误区三:编译只能用
gcc
,链接只能用g++
- 严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用
gcc/g++
,而链接可以用g++
或者gcc -lstdc++
gcc
命令不能自动和C++程序使用的库联接,所以通常使用g++
来完成链接。但在编译阶段,g++
会自动调用gcc
,二者等价
- 严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用
静态库和动态库
(1)什么是库
- 库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类
- 库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行
- 库文件有两种,静态库和动态库(共享库)。区别是:
- 静态库在程序的链接阶段被复制到了程序中
- 动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用
- 库的好处:代码保密和方便部署和分发
(2)静态库的制作

.
├── calc
│ ├── add.c
│ ├── div.c
│ ├── head.h
│ ├── main.c
│ ├── mult.c
│ └── sub.c
└── library
├── include
│ └── head.h
├── main.c
└── src
├── add.c
├── div.c
├── mult.c
└── sub.c
gcc -c add.c div.c mult.c sub.c
ar rcs libcalc.a *.o
cp ./calc/libcalc.a ./library/lib
gcc main.c -o main -I ./include -L ./lib -l calc
(3)动态库的制作

.
├── calc
│ ├── add.c
│ ├── div.c
│ ├── head.h
│ ├── main.c
│ ├── mult.c
│ └── sub.c
└── library
├── include
│ └── head.h
├── main.c
└── src
├── add.c
├── div.c
├── mult.c
└── sub.c
gcc -c -fpic add.c div.c mult.c sub.c
gcc -shared *.o -o libcalc.so
cp ../calc/libcalc.so ./lib/
gcc main.c -o main -I ./include -L ./lib -l calc
./main
./main: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
ldd ./main
linux-vdso.so.1 (0x00007fff47da2000)
libcalc.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000075f138e00000)
/lib64/ld-linux-x86-64.so.2 (0x000075f1390b5000)
如何定位共享库文件呢?
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。对于elf格式
的可执行程序,是由ld-linux.so
来完成的,它先后搜索elf文件
的 DT_RPATH
段 => 环境变量LD_LIBRARY_PATH
=> /etc/ld.so.cache文件列表
=> /lib/
,usr/lib
目录找到库文件后将其载入内存
# 1、修改环境变量,当前终端生效,退出当前终端失效
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/your/library/path
# 2、修改环境变量,用户级别永久配置
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/your/library/path' >> ~/.zshrc
# 3、修改环境变量,系统级别永久配置
sudo vim /etc/profile
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/your/library/path
source /etc/profile
# 4、修改 /etc/ld.so.conf 并更新缓存(系统级配置)
sudo vim /etc/ld.so.conf
/your/library/path
sudo ldconfig
(4)静态库和动态库的对比
程序编译成可执行程序的过程


Makefile
(1)简介
- 一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,
Makefile
文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile
文件就像一个Shell
脚本一样,也可以执行操作系统的命令 Makefile
带来的好处就是“自动化编译” ,一旦写好,只需要一个make
命令,整个工程完全自动编译,极大的提高了软件开发的效率,make
是一个命令工具,是一个解释Makefile
文件中指令的命令工具。
(2)Makefile文件命名和规则
文件命名:
makefile
或者Makefile
Makefile
规则一个
Makefile
文件中可以有一个或者多个规则- 目标:最终要生成的文件(伪目标除外)
- 依赖:生成目标所需要的文件或是目标
- 命令:通过执行命令对依赖操作生成目标(命令前必须 Tab 缩进)
Makefile
中的其它规则一般都是为第一条规则服务的。
目标 ... : 依赖 ...
命令(Shell 命令)
...
(3)基本原理
- 命令在执行之前,需要先检查规则中的依赖是否存在
- 如果存在,执行命令
- 如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令
- 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
- 如果依赖的时间比目标的时间晚,需要重新生成目标
- 如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行
(4)Makefile编写方式
add.c div.c head.h main.c mult.c sub.c
直接编译链接
app:add.c div.c multi.c sub.c main.c gcc add.c div.c multi.c sub.c main.c -o app
编译+链接
app:add.o div.o multi.o sub.o main.o gcc add.o div.o multi.o sub.o main.o -o app add.o:add.c gcc -c add.c -o add.o div.o:div.c gcc -c div.c -o div.o multi.o:multi.c gcc -c multi.c -o multi.o sub.o:sub.c gcc -c sub.c -o sub.o main.o:main.c gcc -c main.c -o main.o
变量
自定义变量:
变量名=变量值
,如var=hello
预定义变量
AR
: 归档维护程序的名称,默认值为 arCC
: C 编译器的名称,默认值为 ccCXX
: C++ 编译器的名称,默认值为 g++$@
: 目标的完整名称$<
: 第一个依赖文件的名称$^
: 所有的依赖文件
获取变量的值:
$(变量名)
,如$(var)
src=add.o div.o multi.o sub.o main.o target=app $(target):$(src) $(CC) $^ -o $@ add.o:add.c $(CC) -c $^ -o $@ div.o:div.c $(CC) -c $^ -o $@ multi.o:multi.c $(CC) -c $^ -o $@ sub.o:sub.c $(CC) -c $^ -o $@ main.o:main.c $(CC) -c $^ -o $@
模式匹配:%.o:%.c
- %: 通配符,匹配一个字符串
- 两个%匹配的是同一个字符串
src=add.o div.o multi.o sub.o main.o target=app $(target):$(src) $(CC) $^ -o $@ %.o:%.c $(CC) -c $< -o $@
函数
$(wildcard PATTERN...)
- 功能:获取指定目录下指定类型的文件列表
- 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔
- 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
- 示例:$(wildcard .c ./sub/.c),返回值格式: a.c b.c c.c d.c e.c f.c
$(patsubst <pattern>,<replacement>,<text>)
- 功能:查找
<text>
中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>
,如果匹配的话,则以<replacement>
替换 <pattern>
可以包括通配符%
,表示任意长度的字串。如果<replacement>
中也包含%
,那么,<replacement>
中的这个%
将是<pattern>
中的那个%所代表的字串。(可以用\
来转义,以\%
来表示真实含义的%
字符)- 返回:函数返回被替换过后的字符串
- 示例:$(patsubst %.c, %.o, x.c bar.c),返回值格式: x.o bar.o
- 功能:查找
src=$(wildcard ./*.c) objs=$(patsubst %.c, %.o, $(src)) target=app $(target):$(objs) $(CC) $^ -o $@ %.o:%.c $(CC) -c $< -o $@
清理中间件:make默认只能构建第一目标,clean是一个伪目标,通常用于手动清理,必须显示调用
src=$(wildcard ./*.c) objs=$(patsubst %.c, %.o, $(src)) target=app $(target):$(objs) $(CC) $^ -o $@ %.o:%.c $(CC) -c $< -o $@ clean: rm *.o #这样 make 才会执行 clean 目标下的命令 rm *.o make clean
GDB
GDB
是由 GNU 软件系统社区提供的调试工具,同GCC
配套组成了一套完整的开发环境,GDB
是 Linux 和许多类 Unix 系统中的标准开发环境一般来说,
GDB
主要帮助你完成下面四个方面的功能- 启动程序,可以按照自定义的要求随心所欲的运行程序
- 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
- 当程序被停住时,可以检查此时程序中所发生的事
- 可以改变程序,将一个 BUG 产生的影响修正从而测试其他 BUG
通常,在为调试而编译时,我们会关掉编译器的优化选项(
-O
), 并打开调试选项(-g
)。另外,-Wall
在尽量不影响程序行为的情况下选项打开所有warning,也可以发现许多问题,避免一些不必要的 BUG。gcc -g -Wall program.c -o program
-g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb
能找到源文件注:当在
gdb
中直接使用回车
时,会默认执行上一条命令

文件IO
在Linux
中使用man 2 API名
查看Linux系统API,man 3 API名
查看标准C库API
标准C库IO函数

标准 C 库 IO 和 Linux 系统 IO 的关系
标准 C 库 I/O 函数最终会调用系统 I/O 函数;C 库 I/O 提供缓冲,减少系统调用次数,系统 I/O 是无缓冲的,每次调用都直接与内核交互。系统 I/O 使用文件描述符 (整数);C 库 I/O 使用 FILE 指针 (结构体指针)

虚拟地址空间
虚拟地址空间是为了解决内存加载问题
- 问题1:假设实际内存为
4G
,此时共有1G
、2G
、2G
三个程序,如果直接加载,那么第三个程序由于内存不足而无法执行 - 问题2:当问题1的
1G
程序执行完后,释放内存,第三个程序可以执行,但此时内存空间不连续
- 问题1:假设实际内存为
对于32位机器来说,大小约为232,即
4G
左右,对于64位机器来说,大小约为248,即256T
左右通过
CPU中的MMU(内存管理单元)
将虚拟内存地址映射到物理内存地址上

文件描述符
- 文件描述符表是一个数组,为了一个进程能够同时操作多个文件
- 文件描述符表默认大小:1024

Linux 系统 IO 函数
open & close
#include <fcntl.h>
int open(const char *pathname, int flags, ...
/* mode_t mode */ );
#include <unistd.h>
int close(int fd);
- 参数
- pathname:要打开/创建的文件的路径
- flags:对文件的操作权限和其他的设置
- 必选项:O_RDONLY, O_WRONLY, O_RDWR 这三个之间是互斥的
- 可选项:O_CREAT 文件不存在,创建新文件
- flags 参数是一个 int 类型的数据,占4个字节,32位,每一位就是一个标志位,所以用
|
可以保证能够实现多个操作
- mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775(最终的权限是:
mode & ~umask
,输入umask可查看umask的值,umask的作用就是抹去某些权限。按位与:0和任何数都为0)
- 返回值:返回一个新的文件描述符,如果调用失败,返回 -1,并设置errno,errno属于Linux系统函数库里面的一个全局变量,记录的是最近的错误号(
#include <stdio.h> void perror(const char *s)
;作用:打印errno对应的错误描述;参数s:用户描述,比如hello, 最终输出的内容是hello:xxx(实际的错误描述))
read & write
#include <unistd.h>
ssize_t read(int fd, void buf[.count], size_t count);
- 参数
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:需要读取数据存放的地方,数组的地址(传出参数)
- count:指定的数组的大小
- 返回值
- 成功:大于0,返回实际的读取到的字节数;等于0,文件已经读取完了
- 失败:-1
#include <unistd.h>
ssize_t write(int fd, const void buf[.count], size_t count);
- 参数
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:要往磁盘写入的数据
- count:要写的数据的实际的大小
- 返回值
- 成功:实际写入的字节数
- 失败:返回-1,并设置errno
lseek
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
- 参数:
- fd:文件描述符,通过open得到的,通过这个fd操作某个文件
- offset:偏移量
- whence:SEEK_SET,设置文件指针的偏移量;SEEK_CUR,设置偏移量,当前位置 + 第二个参数offset的值;SEEK_END,设置偏移量,文件大小 + 第二个参数offset的值。
- 返回值:返回文件指针的位置
- 作用:
- 移动文件指针到文件头:lseek(fd, 0, SEEK_SET);
- 获取当前文件指针的位置:lseek(fd, 0, SEEK_CUR);
- 获取文件长度:lseek(fd, 0, SEEK_END);
- 拓展文件的长度,当前文件10b, 110b, 增加了100个字节:lseek(fd, 100, SEEK_END)[需要写一次数据]
stat & lstat(获取文件信息及软链接信息)
#include <sys/stat.h>
int stat(const char *restrict pathname,struct stat *restrict statbuf);
int lstat(const char *restrict pathname,struct stat *restrict statbuf);
作用:获取一个文件相关的一些信息
参数:
- pathname:操作的文件的路径
- statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
返回值:成功返回0;失败返回-1并设置errno
区别:如果
/path/to/file
是一个指向/real/file
的符号链接,stat()
返回的是/real/file
的信息,lstat
返回的是/path/to/file
的信息。stat结构体
struct stat { dev_t st_dev; // 文件的设备编号 ino_t st_ino; // 节点 mode_t st_mode; // 文件的类型和存取的权限 nlink_t st_nlink; // 连到该文件的硬连接数目 uid_t st_uid; // 用户ID gid_t st_gid; // 组ID dev_t st_rdev; // 设备文件的设备编号 off_t st_size; // 文件字节数(文件大小) blksize_t st_blksize; // 块大小 blkcnt_t st_blocks; // 块数 time_t st_atime; // 最后一次访问时间 time_t st_mtime; // 最后一次修改时间 time_t st_ctime; // 最后一次改变时间(指属性) };
st_mode
image-20250405151951576
文件属性操作函数
access
#include <unistd.h>
int access(const char *pathname, int mode);
- 作用:判断某个文件是否有某个权限,或者判断文件是否存在
- 参数:
- pathname: 判断的文件路径
- mode: R_OK,判断是否有读权限; W_OK,判断是否有写权限;X_OK,判断是否有执行权限;F_OK,判断文件是否存在;
- 返回值:成功返回0,失败返回-1
chmod & chown
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
- 作用:修改文件的权限
- 参数:
- pathname: 需要修改的文件的路径
- mode:需要修改的权限值,八进制的数
- 返回值:成功返回0,失败返回-1
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
- 修改文件所有者
- 可使用
vim /etc/passwd
查看有哪些用户 - 可使用
vim /etc/group
查看有哪些组
truncate
#include <unistd.h>
int truncate(const char *path, off_t length);
- 作用:缩减或者扩展文件的尺寸至指定的大小
- 参数:
- path: 需要修改的文件的路径
- length: 需要最终文件变成的大小
- 返回值:成功返回0,失败返回-1