gcc相关
工作流程
1、预处理 -E
预处理器(cpp)
宏替换
头文件展开
注释去掉
xxx.c -> xxx.i(C文件)
2、编译 -S
编译器(gcc)
xxx.i->xxx.s(汇编文件)
3、汇编 -C
汇编器(as)
xxx.s -> xxx.o(二进制文件)
4、链接
链接器(ld)
xxx.0 -> xxx(可执行文件)
-o的是指明生成文件名字
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
gcc常用参数
-v/--version: 版本号
-I: 编译的时候指定头文件路径
-c: 生成二进制文件,得到.o文件
-o: 指定生成的文件名字
-g: gdb调试的时候需要加
-D: 编译的时候指定一个宏,测试程序时使用
-Wall: 添加警告提示信息
-On: O是优化代码,n是优化级别:1, 2, 3
库
库就是二进制文件
将源代码->二进制格式的源代码
相当于加密操作,别人能用但是不知道内部实现
静态库
命名规则:libxxx(库名).a
如libtest.a
制作步骤:1、原材料:源代码.c/.cpp
2、将.c文件生成.o
3、将.o文件打包,如
语法:ar rcs 静态库的名字 原材料
ar rcs libtest.a *.o
查看静态库内的文件:nm libtest.a
4、库放到lib
目录中,将包含所有头文件的include
目录和lib
目录给用户
库的使用:gcc main.c -I ./include/ -L ./lib/ -ltest -o app
使用两个参数-L和-l
-L 指定库的路径
-l库的名字
动态库
命名规则:libxxx(库名).so
如libtest.so
制作步骤:1、原材料:源代码.c/.cpp
2、将.c文件生成.o
3、将.o文件打包,如
语法:gcc -shared -o 动态库的名字 原材料
gcc -shared -o libtest.so *.o
4、库放到lib
目录中,将包含所有头文件的include
目录和lib
目录给用户
库的使用:gcc main.c -I ./include/ -L ./lib/ -ltest -o app
执行app可执行文件,出现错误,提示未找到库文件
错误原因
对于elf
格式的可执行文件,是由ld-linux.so*
完成的,它先后搜索elf
文件的DT_RPATH
段-------环境变量LD_LIBRARY_PATH
------/etc/ld.so.cache
文件列表-------/lib/
,/usr/libc
目录找到库文件后将其载入内存
如何让系统找到动态库
1、临时设置,LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径
export LD_LIBRARY_PATH=库路径:$LD_LIBRARY_PATH
2、永久设置
系统级别:将export放在/etc/profile
中,也需要重启系统或者执行source /etc/profile
命令
用户级别:将export放在~/.bashrc
中,需要重启终端或者执行source ~/.bashrc
命令
3、更新/etc/ld.so.cache
文件列表
将动态库的绝对路径写到/etc/ld.so.conf
中,执行命令sudo ldconfig -v
重新配置文件列表
4、函数调用调用动态库
dlopen
dlclose
dlsym
静态库,动态库优缺点
静态库
优点:1、静态库被打包到应用程序中加载速度快
2、发布程序无需提供静态库,移植方便
缺点:1、销毁系统资源,浪费内存
2、更新、部署、发布麻烦
动态库
优点:1、可实现进程资源共享
2、程序升级简单
3、程序员可以控制何时加载动态库
缺点:1、加载速度比静态库慢
2、发布程序需要提供依赖的动态库
makefile
make:linux自带的构建器,构建规则在makefile中
makefile中由一条或多条规则组成
makefile文件命名:makefile/Makefile
makefile编写规则:目标,依赖,命令
makefile第一个版本
语法规范:
目标:依赖
(tab缩进)命令
例如:gcc a.c b.c c.c -o app
app:a.c b.c c.c
gcc a.c b.c c.c -o app
缺点:效率低,修改一个文件,所有文件会被全部重新编译
makefile第二个版本
app:a.o b.o c.o
gcc a.o b.o c.o -o app
a.o:a.c
gcc a.c -c
b.o:b.c
gcc b.c -c
c.o:c.c
gcc c.c -c
已解决问题:修改单个文件,该文件重新编译,其他文件不会
缺点:冗余
工作原理
1、检查依赖(文件)是否存在
向下搜索下边的规则,如果有规则用来生成查找的依赖的,执行规则中的命令
2、依赖存在,判断是否需要更新
原则:目标时间 >(大于) 依赖的时间
反之,则更新
makefile第三个版本
自定义变量:obj = a.o b.o c.o
变量的取值:aa=$(obj)
makefile自带的变量:大写
自动变量:$@
表示规则中的目标,$<
表示规则中的第一个依赖,$^
表示规则中所有的依赖,只能在规则的命令中使用
obj = a.c b.c c.c
target = app
$(target):$(obj)
gcc $(obj) -o $(target)
%.o:%.c
gcc -c $< -o $@
缺点:可移植性比较差
makefile第四个版本
makefile所有函数都有返回值
src=$(wildcard)
查找指定目录下指定类型的文件,例如
src=$(wildcard ./*.c)
obj=$(patsubst)
匹配替换,三个参数a,b,c,使用c将a类型替换为b类型,例如
obj=$(patsubst %.c,%.o,%(src))
src = $(wildcard ./*.c)
obj = $(patsubst %.c, %.o, $(src))
target = app
$(target):$(obj)
gcc $(obj) -o $(target)
%.o:%.c
gcc -c $< -o $@
缺点:不能清理项目
makefile第五个版本
让make生成不是终极目标的目标:make 目标名
清理项目的规则语法
clean:
rm *.o app
在终端执行
make clean
注意:在命令前加-
,忽略执行失败的命令,并向下执行其他命令
声明伪目标
需要解决的问题:在目录中使用touch clean
创建clean
文件,会不执行清楚项目的规则
.PHONY:clean
clean:
rm *.o app
gdb调试
gcc a,c b.c -o app
不能gdb调试,没有函数名和变量名,只有地址
gcc a,c b.c -o app -g
,-g
保留函数名和变量名
启动gdb
gdb 可执行程序的名字
,例如gdb app
给程序传参:set args xxx xxxxxx
查看代码
show listsize
显示使用l
命令默认显示的行数
set listsize 20
更改默认显示的行数
l
默认显示mian
函数文件
l 5
显示文件第5行上下的部分
l 函数名
显示文件函数的上下的部分
l 文件名:15
显示指定文件的15行上下的部分
l 文件名:函数名
显示指定文件的函数上下的部分
断点操作
b 15
在main
文件的15行设置断点
i b
查看设置过得断点
d 1
删除断点,1
是断点的编号,可用i b
查看编号
d 4-7
删除多个断点
dis 8
使断点无效,8
是断点的编号,可用i b
查看编号
ena 7
使断点生效,7
是断点的编号,可用i b
查看编号
b 17 if i == 10
在17行设置断点,当i=10
时停止运行
调试相关命令
r
使程序跑起来,到第一个断点处
start
程序跑起来,到程序的第一行
p i
打印i
的值
ptype i
查看i
的类型
n
向下走一步,不会进入函数体
display i
在运行时,自动显示i
的值
i display
显示所有display
的编号
undisplay 1
使编号为1
的自动显示变量失效
continue/c
走多步,到下一个断点
s
进入函数体
finish
出函数体,但是不能打断点
set var i=5
设置变量的值为5,即i=5
,相当于,直接走到i=5
的位置
until
跳出当前循环,但是不能打断点
q
退出
文件IO
C语言的库函数会发生系统调用,调用系统函数
虚拟地址空间
Linux每一个运行的程序(进程)操作系统都会为其分配一个0~4G的地址空间(虚拟地址空间)
文件描述符
一个进程有一个文件描述符表:0~1023
前三个被占用
文件描述符作用:寻找磁盘文件
函数
open
函数原型:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数:
flags:必选项O_RDONLY
O_WRONLY
O_RDWR
可选项
创建文件:O_CREAT
创建文件时检测文件是否存在:O_EXCL
如果文件存在,返回-1
必须与O_CREAT一起使用
追加文件:O_APPEND
文件截断:O_TRUNC
设置非阻塞:O_NONBLOCK
mode:8
进制的数,指定权限
传入的数字权限不一定时最终的权限,mode & ~umask
的值才是最终权限
umask
默认为0002
,可以使用umask 数值
来改变它的临时值
close
函数原型:
#include >unistd.h>
int close(int fd);
read
函数原型:
ssize_t read(int fd, void *buf, size_t count);
size_t
无符号的整型值
ssize_t
有符号的整型值
参数:
fd:open
的返回值
buf:缓冲区,存储要读取的数据
count:缓冲区能够存储的做大字节数sizeof(buf)
返回值:
-1:失败
成功:
>0:读出的字节数
=0:文件读完了
wirte
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:open
的返回值
buf:要写到文件的数据
count:数据的有效字节数strlen(buf)
返回值:
-1:失败
成功:
>0:写入到文件的字节数
lseek
函数原型:
off_t lseek(int fd, off_t offset, int whence);
whence
参数:
SEEK_SET
头部
SEEK_CUR
当前位置
SEEK_END
尾部
// 文件库指针移动到头部
int len = lseek(fd, 0, SEEK_SET)
// 获取文件指针当前的位置
int len = lseek(fd, 0, SEEK_CUR)
// 获取文件长度
int len = lseek(fd, 0, SEEK_END)
实现文件拓展功能
// 文件原大小100k,拓展为1100k
lseek(fd, 1000, SEEK_END);
// 最后需要做一次额外的写操作
write(fd, "a", 1);
全局变量errno
调用函数失败时,设置errno
的值,不同的值对应不同错误,perror()
函数帮助我们打印错误信息,可以传入参数,注释是哪个函数的错误信息
阻塞和非阻塞
阻塞读终端
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
// 阻塞读终端
int main(void)
{
char buf[10];
n = read(STDIN_FILEND, buf, 10);
if (n < 0)
{
perror("read STDIN_FILEND");
exit(1);
}
write(STDOUT_FILEND, buf, n);
return 0;
}
在Linux
系统中执行上面的C代码:
1.执行C
文件,read
函数堵塞,等待输入
2.输入hello, world
,终端write
原因:
1.默认bash
是前台程序
2../a.out
启动了一个程序,前台程序
3../a.out
变成了前台程序,bash
变成了后台程序
4../a.out
等待用户输入10个字符
5.实际输入字符个数大于10,剩下的还在缓冲区
6.read
接触阻塞读缓冲区数据
7.write
写数据到终端
8.程序结束
9.bash
从后台程序变为前台程序,检测到了缓冲区数据,将数据作为shell
命令去执行
非阻塞读终端
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define MSG_TRY "try again\n"
// 非阻塞读终端
int main(void)
{
char buf[10];
int fd, n;
// /dev/tty --> 当前打开的终端设备
fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
perror("open /dev/tty");
exit(1);
}
tryagain:
n = read(fd, buf, 10);
if (n < 0)
{
// 如果write为非阻塞,但是没有数据刻度,此时全局变量errno被设置为EAGAIN
if (errno == EAGAIN)
{
sleep(3);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read /dev/tty");
exit(1);
}
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
在Linux
系统中执行上面的C代码:
1.执行C
文件,每隔3秒读一次终端
2.输入hello
,睡醒检测到,读入输出
结论:1.阻塞和非阻塞是文件的属性
2.普通文件,默认不阻塞
3.终端设备,默认阻塞
4.管道,默认阻塞
5.套接字,默认阻塞
stat函数
ctime:创建时间
atime:访问时间
mtime:修改时间
终端的stat
命令:
函数声明:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char * pathname, struct stat * buf);
int lstat(const char * pathname, struct stat * buf);
pathname
,路径
struct stat{
dev_t st_dev; // 文件的设备编号
ino_t st_ino; // 节点
mode_t st_mode; // 文件的类型和存取的权限
nlink_t st_nlink; // 连到该文件的硬链接数目,刚建立的文件值为1
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_t st_rdev; // (设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; // 文件字节数(文件大小)
blksize_t st_blksize; // 块大小(文件系统的I/O缓冲区大小)
blkcnt_t st_blocks; // 块数
time_t st_atime; // 最后一次访问时间
time_t st_mtime; // 最后一次修改时间
time_t st_ctime; // 最后一次改变时间(指属性)
};
st_mode -- 16位整数
0-2 bit--其他人权限
S_IROTH 00004 读权限
S_IWOTH 00002 写权限
S_IXOTH 00001 执行权限
S_IRWXO 00007 掩码,过滤 st_mode 中除其他人权限以外的信息
3-5 bit--所属组权限
S_IRGRP 00040 读权限
S_IWGRP 00020 写权限
S_IXGRP 00010 执行权限
S_IRWXG 00070 掩码,过滤 st_mode 中除所属组权限以外的信息
6-8 bit--文件所有者权限
S_IRUSR 00400 读权限
S_IWUSR 00200 写权限
S_IXUSR 00100 执行权限
S_IRWXU 00700 掩码,过滤 st_mode 中除文件所有者权限以外的信息
12-15 bit--文件类型
S_IFSOCK 0140000 套接字
S_IFLNK 0120000 符号链接(软链接)
S_IFREG 0100000 普通文件
S_IFBLK 0060000 块设备
S_IFDIR 0040000 目录
S_IFCHR 0020000 字符设备
S_IFIFO 0010000 管道
S_IFMT 0170000 掩码,过滤 st_mode 中除文件类型以外的信息
例如:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main(int argc, const char *argv[])
{
struct stat st;
struct stat* st1;
st1 = &st;
int ret = stat("english.txt", &st);
if (ret == -1)
{
perror("stat error");
exit(1);
}
printf("file size = %d\n", (int)st.st_size);
// 文件类型 - 判断是不是普通文件
if ((st.st_mode & S_IFMT) == S_IFREG)
{
printf("normal file\n");
}
return 0;
}
结果:
lstat和fstat
函数原型:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int lstat(const char * pathname, struct stat * buf);
int fstat(int fd, struct stat * buf);
stat
和lstat
区别:如果读取软链接文件,stat
读的是链接文件指向的文件的属性,lstat
读的是链接文件本身的属性。stat
又称为追踪,穿透。
文件属性相关的函数
access
含义:测试当前用户指定文件是否具有某种属性,当前用户是指调用这个函数的用户
函数原型:
#include <unistd.h>
int access(const char *pathname, int mode);
pathname
,文件名
mode
:4种权限
R_OK
– 读
W_OK
– 写
X_OK
– 执行
F_OK
– 文件是否存在
返回值:
0
– 有某种权限,或者文件存在
1
– 没有,或文件不存在
chmod,修改文件权限
函数原型:
int chmod(const char * filename, int mode);
pathname
,文件名
mode
:文件权限,8
进制数
chown,修改文件所有者和所属组
函数原型:
int chown(const char * path, uid_t owner, gid_t group);
path
,文件路径
owner
,整型值,用户ID
/etc/passwd
group
,整型值,组ID
/etc/group
truncate,修改文件大小
函数原型:
int truncate(const char * path, off_t length);
path
,文件名
length
,文件的最终大小
1,比原来小,删掉后边的部分
2,比原来大,向后拓展
目录操作相关的函数
rename,文件重命名
函数原型:
int rename(const char * oldpath, const char * newpath);
chdir,修改当前进程(应用程序)的路径cd
函数原型:
int chdir(const char *path);
path
,切换的路径
getpwd,获取当前进程的工作目录
函数原型:
char *getcwd(char *buf, size_t size);
buf
,缓冲区,存储当前的工作目录
size
,缓冲区大小
返回值:
成功:当前的工作目录
失败:NULL
mkdir,创建目录
函数原型:
int mkdir(const char * pathname, mode_t mode);
pathname
,创建的目录名
mode
,目录权限,8
进制的数,实际权限:mode&~umask
rmdir,删除一个空目录
函数原型:
int rmdir(const char * pathname);
pathname
,空目录的名字
目录遍历函数
opendir,打开一个目录
函数原型:
DIR *opendir(const char * name);
name
,目录名
返回值:指向目录的指针
读目录
struct dirent
{
ino_t d_ino; // 此目录进入点的inode
ff_t d_off; // 目录文件开头至此目录进入点的位移
signed short int d_reclen; // d_name的长度,不包括NULL字符
unsigned char d_type; // d_name所指的文件类型
har d_name[256]; // 文件名
};
d_type:
DT_BLK -- 块设备
DT_CHR -- 字符设备
DT_DIR -- 目录
DT_LNK -- 软链接
DT_FIFO -- 管道
DT_REG -- 普通文件
DT_SOCK -- 套接字
DT_UNKNOWN -- 未知
struct dirent *readdir(DIR *dirp);
参数 -- opendir的返回值
返回值 -- 目录项结构体
关闭目录
函数类型:
int closedir(DIR *dirp);
循环遍历目录
每次readdir
按照某种规则,对目录内依次读取,每次读取一个文件
dup-dup2
dup,复制文件描述符
函数原型:
int dup(int oldfd);
int dup2(int oldfd, int newfd);
oldfd
– 要复制的文件描述符
返回值:新的文件描述符,取最小的且没被占用的文件描述符
dup
调用成功:有两个文件描述符指向同一个文件