Linux编程系列之权限理解和基础开发工具的使用(下)


前言

接着上一篇博客写完这些基础开发工具,最后用vim写一个进度条代码。尽快过渡到第一座大山---进程。


一、gcc/g++

基础用法和扩展知识

gcc/g++是一款编译器, 用来编译C/C++语言的,先看看怎么使用,比如我写了一个打印hello world。

格式 gcc [选项] 要编译的文件 [选项] [目标文件]

默认编译成a.out 可以带-o,后面提

这个图其实有很多信息,一方面体现出了隐藏目录.的作用,另一方面我们可以猜测指令也是可执行程序!事实上ls、pwd等命令就是可执行程序,被放在了规定的目录下面,不带路径执行时默认先去那个目录里面找!所以直接输入a.out不行,因为它不在默认的目录中,哪个目录呢? /usr/bin/ 换句话说,如果我将我的a.out扔在/usr/bin/ 下面,那我的a.out也可以成为命令!

回到gcc/g++上,之前提到过Linux中文件不区分后缀,不代表gcc/g++不需要区分!gcc只能编译C语言,.c文件,g++既可以编译C也可以编译C++,另外,.cpp .cxx, .cc的后缀均是C++。

生成可执行文件的过程

编译生成的可执行程序不是一步就完成的,具体分成了四步。

  1. 预处理
  2. 编译
  3. 汇编
  4. 连接

1.预处理

预处理阶段主要进行四个操作

a.宏替换 b.去掉注释 c.条件编译 d.展开头文件

上文讲了gcc是可以带选项的!gcc -E main.c -o main.i -E的作用就是预处理后停止, -o目标文件的名字, .i为规定的后缀,也就是预处理过后的C语言程序。我们来用个代码实验一下

条件编译也可以在gcc中带选项,-D宏名字或者-D宏名字=数字或者-D宏名字=字符串,不可以重复定义

看一下生成的main.i文件:

和上文说的一致,另外还有一个小细节,它的头文件写的是/usr/include/stdio.h 但是我们写的时候没有带路径啊,这说明不带路径时系统默认会去/usr/include/下面去找。

2.编译

初学的时候遇到最多的情况应该就是编译错误,这个过程干什么了呢?

在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。vs中可以查看汇编代码

用户可以使用"-S"选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码

gcc -S main.i -o main.o

3.汇编

汇编阶段是把编译阶段生成的".s"文件转成目标文件

读者在此可使用选项"-c"就可看到汇编代码已转化为".o"的二进制目标代码了

gcc --c main.s --o main

这里额外提一点知识,反汇编是什么? ? ? 作用是什么? ? ? 这四个过程中也没有反汇编的事啊,反汇编是为了方便人们调试的,将二进制转化成人类可读的汇编代码,反汇编看的也更加底层,可以看到CPU,寄存器、栈帧的一些细节,核心方便人们去改错。

4.连接

生成可执行文件,连接干了什么呢? 上文提到stdio.h中只包含函数的定义,并没有实现,所以通俗的讲连接就是去找这个函数的实现了,所以一定要有系统默认查找的路径!

系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到

系统默认的搜索路径"/usr/lib"下进行查找,也就是链接到 libc.so.6 库函数中去,这就是链接的作用,这里称之为函数库。

函数库一般分为静态库和动态库两种。

静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为".a",连接的过程也称为静态连接

动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为".so",如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库,可以使用静态库。可以带static选项让他静态连接,使用静态库。
我们可以用ldd或者readelf等命令查看可执行程序用的动态库。

这张图可以明显看出来静态连接编译出来的文本更大

当然我们可以自己制作动静态库,这个会在后面提到。

选项

-E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面

-S 编译到汇编语言不进行汇编和链接

-c 编译到目标代码

-o 文件输出到 文件

-static 此选项对生成的文件采用静态链接

-g 生成调试信息。GNU 调试器可利用该信息。

-shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.

-O0

-O1

-O2

-O3 编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高

-w 不生成任何警告信息。

-Wall 生成所有警告信息。

洛谷中可以看到O2优化,可能会进行一些常量折叠,寄存器优化等操作。

二、gdb

个人感觉,gdb用着很不舒服,vscode的调试也一般,真想调试舒服一点,就去vs,vs的调试还是很舒服的,gdb看看就行。

首先我们要知道:

程序的发布方式有两种,debug模式和release模式

Linux gcc/g++出来的二进制程序,默认是release模式

要使用gdb调试,必须在源代码生成二进制程序的时候, 加上 -g 选项

如何开始和退出呢?

gdb binFile 退出: ctrl + d 或 quit

list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。

list/l 函数名:列出某个函数的源代码。

r或run:运行程序。

n 或 next:单条执行。

s或step:进入函数调用

break(b) 行号:在某一行设置断点

break 函数名:在某个函数开头设置断点

info break :查看断点信息。

finish:执行到当前函数返回,然后挺下来等待命令

print§:打印表达式的值,通过表达式可以修改变量的值或者调用函数

p 变量:打印变量值。

set var:修改变量的值

continue(或c):从当前位置开始连续而非单步执行程序

run(或r):从开始连续而非单步执行程序

delete breakpoints:删除所有断点

delete breakpoints n:删除序号为n的断点

disable breakpoints:禁用断点

enable breakpoints:启用断点

info(或i) breakpoints:参看当前设置了哪些断点

display 变量名:跟踪查看一个变量,每次停下来都显示它的值

undisplay:取消对先前设置的那些变量的跟踪

until X行号:跳至X行

breaktrace(或bt):查看各级函数调用及参数

info(i) locals:查看当前栈帧局部变量的值

quit:退出gdb

调试的思路差不多,整体都是打断点,进调试,然后根据逐语句或者逐过程调试

三、Makefile

这个是一个很有用的工具。

会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力

一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作

makefile带来的好处就是------"自动化编译",一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。

make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建

用法

makefile如何编写?? 核心就是依赖关系 + 依赖方法。文件名叫makefile或者Makefile都可以,先写一个编译cpp的,写法:

第一行叫做依赖关系,main这个可执行文件依赖main.cpp,同时,main也是你的命令,比如make main,就可以执行下面那句话,那你上面说的make指令是什么意思?在makefile中,第一个依赖关系,可以只用make执行依赖方法! 你的依赖方法可以有多行,但是必须先空两个格(按一下Table) ,再写依赖方法,另外:依赖方法可以为空! 依赖关系中可以只有前面,冒号后面可以不跟任何东西,因为不是所有命令都需要依赖的,比如后面说的清理!

那上面刚讲完生成可执行文件的过程,是不是可以使用makefile一步一步的编译出来呢?当然可以!核心还是控制好依赖关系和依赖方法。

Makefile 复制代码
main:main.o
	g++ main.o -o main
main.o:main.s
	g++ -c main.s -o main.o
main.s:main.i
	g++ -S main.i -o main.s
main.i:main.cpp
	g++ -E main.cpp -o main.i

原理

任意几句调换顺序可以吗?? 也可以,这就涉及到了原理,比如输入make指令。

  1. make会在当前目录下找名字叫"Makefile"或"makefile"的文件。
  2. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到"main"这个文件,并把这个文件作为最终的目标文件。
  3. 如果main文件不存在,或是hellomain依赖的后面的hello.o文件的文件修改时间要比main这个文件新(可以用 touch 测试),那么,他就会执行后面所定义的命令来生成hello这个文件。
  4. 如果main所依赖的main.o文件不存在,那么make会在当前文件中找目标为main.o文件的依赖性,如果找到则再根据那一个规则生成hello.o文件。(这有点像一个堆栈的过程)
  5. 当然,你的C文件和H文件是存在的啦,于是make会生成 main.o 文件,然后再用 main.o 文件声明make的终极任务,也就是执行文件hello了。
  6. 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
  7. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。
  8. make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

所以,顺序不重要,能找到对应的依赖关系很重要。

make clean和伪目标

既然有项目生成,就有项目清理,清理不需要依赖关系,这么写就可以。

makefile 复制代码
clean:
	rm -rf main.o main.i main.s main

这里又引入一个新话题,伪目标,使用make的时候可能会看到这么一句话

已经被更新了,make指令失败了。原理和原因我们后面马上提,但是我不想让这个命令失败,我想让对应的依赖方法总是被执行,怎么办?伪目标

写法:

这样就能保证make clean总是被执行的了!可以尝试在编译中添加,这样保证每次编译都能成功

补充知识

1.一次依赖关系中可能依赖多个文件,比如后面我们制作动态库的时候,libmymath.so:add.o sub.o mul.o 那我就得把add.o xxxx一大串都写上??

makefile也想到了这一点,所以有@ 和 ^ 这两个特殊符号, @代表冒号左边的东西, ^代表冒号右边的一串东西,就可以这么写

gcc -shared add.o sub.o mul.o 1.o 2.o 3.o -o libmymath.so等价于

gcc -shared \^ -o @

如图所示,make的时候,把 \^ 和 @的内容自动替换了进去

当然,也可以这么写g++ -o @ ^ ,这是一样的,写法问题,就和rm -rf * 和 rm * -rf 是一个道理

2.如果想一次同时编译形成两个可执行程序,怎么搞?在后面学习网络书写客户端和服务端要用上的。利用依赖关系,比如:all:client server 依赖方法为空,因为也不需要依赖方法,client和server正常写依赖方法和依赖关系就行!

  1. 如果一个文件的名字太长,可以取别名,比如:
bash 复制代码
m=main.cpp

main:$m
	g++  -o $@ $^ -std=c++11 -lpthread

$就是取出里面的值,比如查看环境变量的值.

4.上面我们提到,make指令失败的情况,这里我们来提一下原因和原理

原因:提高编译效率,这次编译的结果和上次一模一样,那我为什么还要编译??没必要。

原理:源文件经过编译形成可执行文件,那么源文件的最近修改时间一定比可执行文件的最近修改时间要短!

如果我们修改了源文件,可执行还没有被重新编译,那么源文件的最近修改时间大于可执行文件的最近修改时间! 所以,只需要判断这两个时间的比较即可。这就说明,一定有东西记录了这个文件的最近修改时间!这个在文件系统里会提,本质上是存放在了Block块中。

stat指令查看

这里有很多信息,IO Block: 文件系统分配粒度,一次分配4KB。

Blocks:实际分配大小,size是99,不满4kb,分配了4kb,Blocks * 512是字节数,也就是4096字节,4kb。当然这两个不是重点,重点看接下来显示出的三个时间戳,Access,Modify,Change。

文件 = 文件内容 + 文件属性。

1.对文件内容的修改就是Modify,对文件属性的修改就是Change,Access称为访问时间,一般是指最近一次读取的时间,比如cat了一下。

2.一般来说,Modify改变,Access和Change跟着一起改变,因为你更改文件内容,一定要读取,文件的属性比如size这些也会跟着改变。

3.Change改变,Modify和Access一般不变,比如修改权限,只有Change时间改变了,文件内容没有改变,也没有读取文件。

4.一般来说,Change会跟着Access和Modify一起改变,因为Change是记录文件属性的修改情况,这两个时间戳包含在文件属性之内。

5.touch命令的默认行为就是更改Modify和Access时间,Change由于属性变化会跟着改变,如果只想改变Modify,touch -m, 改变Access,touch -a,无论是哪个Change都会跟着变化。

另外还有个问题,你可能在cat文件之后查看,发现access时间没有改变,这实际上是Linux系统采用了默认的access time优化策略,因为正常来说访问一个文件是很频繁的,如果每次都更新就会带来额外的开销。可以通过mount命令查看,其中查看到的relatime就是默认选项,它只会在这两个情况下更新:

1.文件的modify/change时间晚于当前access时间;

2.距离上次access时间更新已超过 24 小时。

这就是cat命令 access时间没有改变的原因。

四、Cmake

这里只简单介绍一下,核心还是方便使用,Cmake是一款工具,核心就是生成makefile文件,不需要自己写,其中有各种各样的方法去规定makefile的样子,文件名:CMakeLists.txt,比较简单的写法:

CMakeLists.txt 复制代码
cmake_minimum_required(VERSION 3.10)

project(Reactor_Server)

set(CMAKE_CXX_STANDARD 11)

add_executable(Reactor_Server Main.cc)
target_link_libraries(Reactor_Server jsoncpp)


add_executable(Client Cal_Client.cc)

第一行是需要的版本,第二行是文件名,第三行是采用C++11标准,第四行是生成可执行文件用Main.cc 编译,第五行是连接指定库,Reactor_Server连接jsoncpp这个库。

写完之后,cmake . 指令运行,或者新开一个目录使得Cmake生成的文件在这里,cmake ...即可。就会看到有一个Makefile了。

五、模拟进度条

前置知识

就是写一个类似下载一样的进度条,一方面可以加强vim的使用,另一方面可以先了解缓冲区的一些知识,在后面的IO中也会提到。

首先我们要知道\r和\n的区别,\r是回车,就是光标回到开始,\n是换行,光标切换到下一行的同一位置。C语言中,\n的概念是换行,但是实际实现中,\r一般被解释成回车 + 换行,\r仍然是单纯的回车。

为什么我输出了两个hello world只显示出来一个呢?就是因为\r 只是回车,然后第二个hello world,将原来的hello world进行了覆盖!再来看一个现象:

cpp 复制代码
#include<bits/stdc++.h>
#include<unistd.h>
using namespace std;

int main()
{
    cout << "hello world";
    sleep(3);

    return 0;
}

sleep是库函数,字面意思。

现象是先sleep(3)秒,再输出hello world,但是是先执行的hello world,再sleep啊,这说明hello world一定被保存起来了!实际上是保存在用户级别的缓冲区内了!这是C语言为我们维护的一段内存。

如果我想强制输出呢?刷新缓冲区即可,C语言中可以用fflush函数,这里先提一句,C标准库为我们提供了三个标准输出流,分别是标准输入,标准输出,标准错误,这里我们要刷新标准输出,fflush(stdout).

C++中,cout有一个成员函数flush,所以直接cout.flush()就可以.

代码

有这种前置知识下,就可以开始写进度条了。

进度条的原理很简单,就是一个字符串每次都变长,利用\r的特点让他每次都覆盖,看成进度条的效果。使用printf方便一点,cout不太方便打印。

代码比较简单,这里只提几个注意事项,因为\和%是转义字符,所以规定字符\是\,%是%%, 对于打印,因为进度条是打印固定长度,只是中间的#不断变长,所以要控制固定宽度,比如100,但是又因为默认是右对齐,我们这里要左对齐,所以要带-。

cpp 复制代码
#define TOP 100
#define STYLE '#'
#define RIGHT ']'
const char s[] = {'-','\\','/', '|' };
void pbar(unsigned int time)
{
   printf("进度条:\n");
   char str[102];
   memset(str,'\0',sizeof(str));
   int cnt = 0;
   int len = strlen(s);
   while(cnt <= TOP)
   {
       printf("[%-100s][%d%%][%c]\r",str,cnt,s[cnt % len]);
       fflush(stdout);
       str[cnt++] = STYLE;
       if(cnt < 100) str[cnt] = RIGHT;
       sleep(time);
   }
  printf("\n");
}
相关推荐
Jelly-小丑鱼2 小时前
Linux搭建SQLserver数据库和Orical数据库
linux·运维·数据库·sqlserver·oracal·docker容器数据库
Run_Teenage2 小时前
Linux:进程等待
linux·运维·服务器
Trouvaille ~2 小时前
【Linux】从磁盘到文件系统:深入理解Ext2文件系统
linux·运维·网络·c++·磁盘·文件系统·inode
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]file
linux·笔记·学习
___波子 Pro Max.2 小时前
Linux ps命令-ef参数详解
linux
春日见2 小时前
眼在手上外参标定保姆级教学(vscode + opencv)
linux·运维·服务器·数码相机·opencv·ubuntu·3d
xwill*2 小时前
python 字符串拼接
linux·windows·python
TracyGC4 小时前
Linux环境-RTX5080显卡CUDA12.8下安装mmcv/mmdetection3d
linux·运维·服务器
xu_ws4 小时前
Linux下快速安装配置Redis全攻略
linux·运维·服务器