Linux环境下的编辑器
在Linux环境下有很多编译器,例如基于行的编辑器ed和ex, 基于文本的编辑器Vim、 Emacs等。使用文本编辑器可以帮助用户翻页、移动光标、查找字符、替换字符、删除等操作。本节中对Vim编辑器进行详细的介绍,并简单介绍其他的编辑器。
Vim使用简介
Vi 是UNIX系统下最通用的文本编辑器。Vi不是一 个所见即所得的编辑器,如果要进行复制和格式化文本需要手动输入命令进行操作。安装好Linux操作系统后, 一 般已经默认安装了Vi编辑器。为了使用方便,建议安装 Vi的扩展版本Vim, 它比Vi更强大,更加适合初学者使用。
1.Vim的安装
apt-get install vim
在Ubuntu下,apt-get工具对系统的软件包进行管理。install选项安装会自动查找和安装指定的软件包,其命令如下:
apt-get install 软件包名称
2.Vim编辑器的模式
-
Vim主要分为普通模式和插入模式。普通模式是命令模式,插入模式是编辑模式。 -
在插入模式下可以进行字符的输入,输入的键值显示在编辑框中,这些文本可以用于编辑。普通界面是进行命令操作的,输入的值代表一个命令。例如在普通模式下按h键,光标会向左移动一个字符的位置。 -
插入模式和普通模式的切换分别为按i键和Esc键。在普通模式下按i键,会转入插入模式;在插入模式下按Esc键进入普通模式。在用户进入Vim还没有进行其他操作时,操作模式是普通模式。
使用Vim建立文件
Vim的命令行格式为"vim文件名",文件名是所要编辑的文件名,例如要编辑一个"hello.c”的C文件,按照如下所述的步骤进行操作:
-
建立文件:vim helloc.c -
进入插入模式:打开文件后,默认是普通模式,按i键,进行插入模式。注:要回到普通模式只需按Esc键,有时按两次Esc键,Vim发车“嘀”一声,就表示Vim在普通模式下了。 -
文本输入:#include <stdio.h> int main(void){printf("Hello World!\n"); return 0;} -
退出Vim:编译完成后,按Esc键退出插入模式回到普通模式,输入":wq"退出Vim编辑器。 -
使用 ls -l 查看创建当前目录下的文件
使用Vim编辑文本
Vim的编辑命令有很多,如在Vim下移动光标,进行删除字符、复制、查找、转跳等操作。
1.移动光标 h、j、k、l
2.刪除字符x、dd、u、Ctrl+R
-
x:要删除一个字符可以使用x,普通模式下,将光标移动到需要删除的字符上面然后按x键。 -
dd:要删除一整行可以使用dd。 -
u:恢复删除可以使用u。 -
Ctrl+r:取消一个命令,可以将之前撤销的字符重新找回。
3.复制粘贴p、y
-
p:粘贴命令,将内存中的字符复制到当前光标的后面。使用前提是内存中有合适的字符串复制。例如:要将一行复制到某个地方,可以使用dd命令删除它,然后使用u命令恢复,这时内存中是dd命令删除的字符串。将光标移动到需要插入的行之前,使用p命令可用把内存中的字符串复制后放置在选定的位置。 -
y:复制命令,将指定的字符串复制到内存中,yw命令用于复制单纯,可用指定复制的单词数量,y2w复制两个单词。例如:#include <stdio.h> 光标位于此行的头部,当输入y2w时字符串#include就复制到内存中,按p键后,如下:##include include <stdio.h> -
注:命令y在进行字符串复制的时候包含末尾的空格。按行进行字符串的复制时,使用dd命令复制的方式比较麻烦,可以使用yy命令进行复制。
4.查找字符串"/"
查找字符串的命令是**“/xxx”**,其中xxx代表查找的字符串。例如:查找当前文件的printf字符串,可以输入下面命令:
:/printf
找到匹配字符串,光标在第一个合适的字符串光标上。查找其他匹配的字符串可以输入"n"向下移动到一个匹配的字符串上,输入字符“N”则会向上移动一个匹配的字符串上。
5.调到某一行 g
命令":n"可以让光标跳到某一行,其中n表示跳到的行数。另一种方式:命令nG,n为跳转的行数,5G是跳转到第5行命令,G大写。
Vim的格式设置
Vim设置缩进,设置Tab键对应空格的长度,设置行号等。
1.设置缩进
set cindent shiftwidth=n
2.设置Tab键的空格数量
set tabstop = n
3.设置行号
set number
注:Vim会根据**~/.vimrc文件设置vi的设置,可以修改文件.vimrc**来定制Vim。
Linux下的GCC环境编辑器
GCC是Linu下的编辑工具集,不仅可以编译C/C++语言,其他例如Objective-C、Pascal、Fortran、Java、Ada等语言。
GCC的C编译器是gcc,其命令如下: GCC支持默认扩展名策略,下表是GCC默认文件扩展名的含义:
GCC下有很多编辑器,可以支持C语言、C++语言等多种语言,下表是常用的: 进行程序编译的时候,头文件路径和库文件是编译器默认查找的地方:
单个文件编译执行文件
使用gcc命令后面加上编译的C语言的源文件,GCC自动生成文件名为a.out的可执行文件。
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
retrun 0;
}
编译成可执行文件:
gcc hello.c
首先将C文件编译成目标文件,然后将目标文件链接成可执行文件,最后删除目标文件。上述没有生成执行文件的名称,GCC将生成默认的文件名a.out。运行结果如下:
./a.out
hello World!
生成指定可执行文件名,用-o,如下面生成test文件名:
gcc -o test hello.c
运行可执行文件:
./test
Hello World!
注:直接生成可执行文件的编译方法,中间文件作为临时文件操存在,在可执行文件生成后,会删除中间文件。
编译生成目标文件
GCC的**-c选项用于生成目标文件,生成的目标文件和源文件名称一样,只是扩展名为.o**。例:下面生成hello.o文件
gcc -c hello.c
生成指定文件名,使用-o,例如:将源文件hello.c编译目标文件,文件名为test.o:
gcc -c -o test.o hello.c
可以使用一条命令编译多个源文件,生成目标文件,例:一个项目包含file1.c、file2.c和file.c,生成三个目标文件:file1.o、file2.o和file3.o:
gcc -c file1.c file2.c file3.c
多文件编译
GCC可以自动编译链接多个文件,不管是目标文件还是源文件,都可以使用同一个命令编译到一个可执行文件中。例如:一个项目包含两个文件,文件string.c中有一个函数StrLen用于计算字符串长度,mian.c中调用这个邯郸将计算的结果显示出来。
①.源文件string.c
#define ENDSTRING '\0'
int StrLen(char *string)
{
int len =0;
while(*string++ !=ENDSTRING)
len++;
retrun len;
}
②源文件 main.c
# include <stdio.h>
extern int Strlen(char* str);
int main(void)
{
char src[] = "Hello Dymatic";
pritnf(string length is:%d\n",Strlen(src));
return 0;
}
③.编译运行
下面将两个源文件中的程序编译成一个执行文件,文件名为:test
gcc -o test string.c main.c
运行:
./test
string length is :13
另一种方法:将源文件编成目标文件,然后进行链接。例:下面将string.c和main.c源文件编译成目标文件string.o和main.o,然后将string.o和main.o链接生成test:
gcc -c string.c main.c
gcc -o test string.o main.o
预处理
C语言程序中,通常需要包含头文件并会定义一些宏,预处理过程将源文件中的头文件包进源文件中,并且将文件中定义的宏进行扩展。 编译程序时选项**-E**,进行预编译操作。例如如下文件string.c的预处理结果显示在计算机屏幕上:
gcc -E string.c
如果需要指定源文件预编译后生成的中间结果文件名,需要使用**-o**,下面将string.c进行预编译,生成string.i。string内容如下:
这时宏ENDSTRING,已经被替换称"\0"。
编译成汇编语言
生成汇编语言GCC选项是**-S**,默认情况下生成的文件名和源文件一致,扩展名为**.s**。
例:将C语言源文件string.c编译成汇编语言,文件名为string.s
gcc -S string.c
生成使用静态链接库
静态库是obj文件的一个集合,通常静态库以".a"为后缀,静态库由程序ar生存。
-
静态库的优点是可以在不用重新编译程序库代码的情况下,进行程序的重新链接,这种方法节省了编译过程的时间(在编译大型程序的时候,需要花费很长时间)。但是由于现在系统的强大,编译的时间已经不是问题。 -
静态库的另一个优势是开发者可以提供库文件给使用的人员,不用开放源代码,这是库函数提供者经常采用的手段。 -
当然这也是程序模块化开发的 一 种手段,使每个软件开发人员的精力集中在自己的部分。在理论上,静态库的执行速度比共享库和动态库要快 (1% ~5% )。
①.生成静态链接库
- 生成静态库,或者将一个obj文件加到已经存下的静态库的命令为"ar库文件obj文件1 obj文件2"。
- 创建静态库的最基本步骤是生成目标文件。
- 然后使用工具ar对目标文件进行归档。
- 工具ar的-r选项,可以创建库,并把目标文件插入到指定库中。
例如:将string.o打包为库文件libstr.a的命令为:
ar -rcs libstr.a string.o
②.使用静态链接库
在编译程序的时候经常需要使用函数库,例如经常使用的C标准库等。GCC链接时使用库函数和 一 般的obj 文件的形式是一 致的,例如对 main.c 进行链接的时候,需要使用之前已经编译好的静态链接库 libstr.a, 命令格式如下:
gcc -o test main.c libstr.a
也可以使用命令"-l库名"进行,库名是不包含函数库和扩展名的字符串。例如:编译main.c链接静态库libstr.a的命令可用修改为:
gcc -o test main.c -lstr
上面的命令将系统默认的路径下查找str函数库,并把它链接到要生成的目标程序上。可能系统提示无法找到库文件str,这是由于str库函数没有在系统默认的查找路径下,需要显示指定库函数的路径,例如库文件和当前编译文件在同一目录下:
gcc -o test main.c -L./ -lstr
注:在使用**-l选项时,-o选项的目的名称要在-l链接的库名称之前,否则gcc会认为-l**是生成目标而出错。
生成动态链接库
-
动态链接库是程序运行时加载的库,当动态链接库正确安装后,所有的程序都可以使用动态库来运行程序。 -
动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址是相对地址,不是绝对地址,其真实地址在调用动态库的程序加载时形成。 -
动态链接库的名称有别名(soname)、真名(realname)和链接名(linkername)。 -
别名由一 个前缀lib, 然后是库的名字,再加上一 个后缀".so"构成。真名是动态链接库的真实名称,一 般总是在别名的基础上加上一 个小版本号、发布版本等构成。 -
除此之外,还有一个链接名,即程序链接时使用的库的名字。在动态链接库安装的时候,总是复制库文件到某个目录下,然后用一 个软链接生成别名,在库文件进行更新的时候,仅仅更新软链接即可。
①.生成动态链接库
生成动态链接库,使用-fPIC选项或者-fpic选项。-fPIC和-fpic选项的作用是使得gcc生成的代码是位置无关的,例如下面得命令将string.c编译生成动态链接库:
gcc -shared -Wl,-soname,libstr.so -o libstr.so.l string.c
选项**“-soname,libstr.so"表示生成动态库的别名是libstr.so:”-o libstr.so.l"选项则表示是生成名字为libstr.so.l的实际动态链接库;-shared**表示格式编译器生成一个动态链接库。
生存动态链接库,一般复制到系统默认的动态链接库的搜索路径,通常有**/llib、/usr/lib、/usr/local/lib**,放在如何目录下都可以。
②.动态链接库的配置
动态链接库不能随便使用,要在运行的程序中使用动态链接库,需要指定系统的动态链接库搜索路径,让系统找到运行所需的动态链接库才可以,系统中的配置文件**/etc/ld.so.conf**是动态链接库的搜索路径配置文件。
在这个文件中,存放着可被Linux共享的动态链接库所在目录的名字(系统目录**/lib、/usr/lib**除外),多个目录名间以空白字符(空格、换行等)或冒号或逗号分隔。查看系统中的动态链接库配置文件的内容:
cat /etc/ld.so.conf
Ubunutu的配置文件将目录/etc/ld.so.conf.d中的配置文件包含进来,对这个目录下的文件进行查看:
ls /etc/ld.so.conf.d/
③.动态链接库管理命令
运行动态链接库的管理命令:ldconfig,ldconfig命令的作用是在系统的默认搜索路径,和动态链接库配置文件中所列出的目录里搜索动态链接库,创建动态链接库装入程序重要的链接和缓存文件。搜索完毕后,将结果写入缓存文件**/etc/ld.so.cache**中。ldconfig命令使用用法如下:
例: ldconfig -p :查看缓存文件中的动态链接库表。
ldconfig -v:将ldconfig在运行过程扫描的目录和共享库信息输出:
查看指定要扫描的目录,并将动态链接库放入系统中进行共享:
ldconfig 目录名
例:将扫描当前用户的lib目录,将动态链接库加入系统:
ldconfig ~/lib
注:如果在运行上述命令后,再次运行ldconfig而没有加参数,系统会将**/lib、/usr/lib及/etc/ld.so.conf**中指定目录中的动态链接库加入缓存,这时候上述代码中的动态链接可能不被系统共享了。
④.使用动态链接库
使用动态库:使用"-l库名",例如:将源文件main.c编译成可执行文件test,并链接库文件libstr.a或者libstr.so:
gcc -o tetst main.c -L./ -lstr
-L指定链接动态链接库的路径,-lstr链接库函数str。但运行test一般会出现而下问题: 由程序运行时没有找到动态链接库造成的,程序编译时链接动态链接库和运行时使用动态链接库的概念不同,在运行时,程序链接的动态链接库需要在系统目录下才行。解决方法:
-
将动态链接库的目录放哪程序搜索路径中,可以将库的路径加到环境变量LD_LIBRARY_PATH中实现,例: 将存放库文件libstr.so的路径**/example/ex02**加入到搜索路径中,再运行程序就没有之前的警告了。 -
第二种方法使用ld-Linux.so.2来加载程序: 加载test程序的命令:
注:如果系统到搜索路径下同时存在静态链接库和动态链接库,默认情况下会链接动态链接库。如果需要强制链接静态链接库,需要加上"-static"选项,上述编译方法改为如下:
动态加载库
动态加载库和一般的动态链接库所不同的是,一般动态链接库在程序启动的时候就要寻找动态库,找到库函数,而动态加载库用程序的方法来控制什么时候加载。动态加载库主要函数dlopen()、dlerror()、dlsym()和dlclose()。
①.打开动态库dlopen()函数
void * dlopen(const char *filename,int flag);
注:filename:动态链接库的文件名,flag:打开方式,一般为RTLD_LASY,函数的返回值为库的值。
②.获得函数指针dlsym(),获得动态链接库中指定函数的指针,然后使用这个函数指针进行操作。
void * dlsym(void *handle,char *symbol);
注:handle为打开动态库返回的句柄,参数symbol为函数名称,返回值为函数指针。
③.使用动态加载库的一个例子
首先使用函数dlopen()打开动态链接库,判断是否正常打开,使用函数dlerror()判断错误。使用dlsym()获得动态链接库中的某个函数。
#include<dlfcn.h>
int main(void)
{
char src[] = "Hello Dymatic";
int(*pStrLenFun)(char *str);
void *phandle = NULL;
char *perr = NULL;
phandle = dlopen("./libstr.so",RTLD LAZY);
if(!phandle)
{
printf("Failed Load library!\n");
}
perr = dllerror();
if(perr !=NULL)
{
printf("%s\n",perr);
return 0;
}
pStrLenFun = dlsym(phandle,"StrLen");
perr = dlerror();
if(perr !=NULL)
{
printf("%s\n",perr);
return 0;
}
printf("the string length is:%d\n",pStrLenFun(src));
dlcose(phandle);
return 0;
}
需要使用链接动态库libdl.so,编译可执行文件testdl。命令将main.c编译成可执行文件testdl,并链接动态链接库libdl.so:
gcc -o testdl main.c libstr.so -ldl
执行文件testdl:
./testdl
GCC常用选项
GCC的选项配置是编译时很重要的选择,例如头文件路径、加载库路径、警告信息及调试等。
-DMACRO选项
定义一个宏,在多种预定义的程序中会经常使用。如下面根据系统是否定义Linux宏来执行不同的代码。使用**-D选项可以选择不同的代码段,例如:-DOS_LINUX**选项将指向代码段①:
- -Idir:将头文件的搜索路径括大,包含dir目录。
- -Ldir:将链接时使用的链接库搜索路径扩大,包含dir目录,gcc都会优先使用共享程序库。
- -static:仅选用静态程序库进行链接,如果一个目录中静态库和动态库都存在,则仅选用静态库。
- -g:包括调试信息。
- -On:优化程序,速度变快,空间占用小,优先级的级别可以选择,即n。最常用的优化级别是2。
- -Wall:打开所有gcc能够提供的、常用的警告信息。
GCC的常用选项及含义
编译环境的搭建
查看是否安装GCC:
which gcc
安装:
apt-get install gcc
注:也可以安装C++的g++。
Makefile文件简介
使用 GCC 的命令行进行程序编译在单个文件下是比较方便的,当工程中的文件逐渐增多,甚至变得十分庞大的时候,使用 G CC 命令编译就会变得力不从心。Linux 中的make 工具提供了一 种管理工程的功能,可以方便地进行程序的编译,对更新的文件进行重新编译。
一个多文件的工程例子
有一个工程中的文件列表如下图所示。工程中共有5个文件,在add 目录中有add_int.c和 add_float.c, 两个文件分别计算整型和浮点型的相加;在 sub 目录下有文件 sub_int.c 和sub_float.c,分别计算整型和浮点型的相减;顶层目录有main.c文件负责整个程序。
文件main.c
#include<stdio,h>
#include "add.h"
#include "sub.h"
int main()
{
int a=10,b=12;
float x=1.23456,y=9.87654321;
printf("int a+b:%d\n",add_int(a,b));
pritnf("int a-b:%d\n",sub_int(a,b));
printf("flaot x+y:%f\n",add_float(x,y));
printf("float x-y:%f\n",sub_float(x,y));
return 0;
}
加操作:
#ifndef __ADD_H__
#define __ADD_H__
extern int add_int(int a,int b);
extern float add_float(float a,float b);
#endif
文件add.float.c的代码如下:
float add_float(float a,float b)
{
return a+b;
}
文件add_int.c的代码如下:
int add_int(int a,int b)
{
return a+b;
}
减操作:
#ifndef __SUB_H__
#define __SUB_H__
extern float sub_float(float a,float b);
extern int sub_int(int a,int b);
#endif
文件sub_int.c的代码如下:
int sub_int(int a,int b)
{
return a-b;
}
文件sub_float.c的代码如下:
float sub_float(float a,float b)
{
return a-b;
}
多文件工程的编译
将上面的多工程文件编译成可执行的文件有两种方法,一种是命令行操作,手动收入将源文件编译Wie可执行文件;另一种是编写Makefile文件,通过make命令将多个文件编译为可执行文件:
①.命令行编译程序
将此文件编译为可执行文件cacu,如果用gcc进行手动编译,是比较麻烦的。例如:下面编译的方式编译·一个C文件,生成目标文件,最后将5个目标文件编译可执行文件:
gcc -c add/add_int.c -o add/add_int.o
gcc -c add/add_float.c -o add/add_float.o
gcc -c sub/sub_int.c -o sub/sub_int.o
gcc -c sub/sub_float.c -o sub/sub_float.o
gcc -c main.c -o main.o
gcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o mian.o
或者使用gcc的默认规范,使用一条命令直接生成可执行文件cacu:
gcc -o cacu add/add_int.c add/add_float.c sub/sub_int.c sub/sub_float.c main.c
②.多文件地Makefile
上面命令行存在缺陷,如果频繁修改源文件或者当项目中的文件比较多、关系复杂时,用gcc直接进行编译会十分困难。 使用make进行项目管理,需要一个Makefile文件,make在进行编译时,从Makefile文件中读取设置情况,进行解析后运行相关的规则。make程序查找当前目录下的文件Makefile或者makefile,按其规则运行,例如:建立如下规则的Makefile文件。
#生成cacu,":"右边为目标文件
cacu:add_int.o add_float.o sub_int.o sub_float.o main.o
gcc -o cacu add/add_int.o add/add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o
#生成add_int.o的规则,将add_int.c编译成目标文件add_int.o
add_int.o:add/add_int.c add/add.h
gcc -c -o add/add_int.o add/add_int.c
#生成add_float.o的规则
add_float.o:add/add_float.c add/add.h
gcc -c -o add/add_float.o add/add_float.c
#生成sub_int.o的规则
sub_iint.o:sub/sub_int.c sub/sub.h
gcc -c -o sub/sub_float.o sub/sub_float.c
#生成sub_float.o的规则
sub_float.o:sub/sub_float.c sub/sub.h
gcc -c -o sub/sub_float.o sub/sub_float.c
#生成main.o的规则
main.o:main.c add/add.h sub/sub.h
gcc -c -o main.c -Iadd -Isub
#清理的规则
clean:
rm rf cacu add/add_int.o add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o
多文件的编译
编译多个文件的项目,在上面Makefile文件编写完毕后,运行make命令:
make
默认情况下执行Makefile中第一个规则,即cacu相关的规则,而cacu规则依赖于多个目标文件add_int.o、add_float.o、sub_int.o、sub_float.o、main.o编译器会先生成上述目标文件后执行下述命令:
(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
上述命令将多个目标文件编译成可执行文件cacu,即:
gcc - o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o -Iadd -Isub -02
命令make clean会调用clean相关的规则,清除编译出来的目标文件,以及cacu。例:
make clean
rm -f cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o
clean规则会执行"-$(RM)$(TAGET)$(OBJS) "命令,将定义的变量扩展后为:
rm -f cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o
Makefile的规则
Makefile的框架是由规则构成的。make命令执行先在Makefile文件中查找各种规则,对各种规则进行解析后运行规则。规则的基本格式为:
-
TARGET: 规则所定义的目标。通常规则是最后生成的可执行文件的文件名或者为了生成可执行文件而依赖的目标文件的文件名,也可以是是一个动作,称之为"伪目标"。 -
DEPENDEDS: 执行此规则所必需的依赖条件,例如生成可执行文件的目标文件。DEPENDEDS 也可以是某个TARGET, 这样就形成了 TARGET 之间的嵌套。 -
COMMAND: 规则所执行的命令,即规则的动作,例如编译文件、生成库文件、进入目录等,动作可以是多个,每个命令占一行。
写好Makefile需要注意:
-
规则的书写:使用反斜杠(\)将较长的行分解;命令行必须以Tab键开始,make程序把程序出现在一条规则之后的所有连续的以Tab键开始的行都作为命令处理。 -
目标:指具体的文件,可以指某个动作。例如目标cauc就是生成cacu的规则,有很多的依赖项的命令动作,而clean是清除当前生成文件的一个动作,不会生成任何目标项。 -
依赖项:依赖项是目标生成所必须满足的条件,例如生成cacu需要依赖main.o,main.o必须存在才能执行生成cacu的命令,即依赖项的动作在TARGET的命令之前执行。依赖项之间的顺序按照自左向右的顺序检查或者执行。例:下面的规则: main.c、add/add.h 和 sub/sub.h 必须都存在才能执行动作 "gcc -c -o main.o main.c -Iadd -Isub "当add/add.h不存在时,是不会执行规则的命令动作的,而且也不会检查sub/sub.h文件的存在,当然 main.c由于在add/add.h依赖项之前,会先确认此项没有问题。 -
规则的嵌套:规则之间是可以嵌套的,这通常通过依赖项实现。例如生成 cacu的规则依赖于很多的**.o文件,而每个.0文件又分别是 一 个规则。要执行规则 cacu 必须先执行它的依赖项,即add_int.o、add_float.o、sub_int.o、sub_float.o、main.o**, 这5个依赖项生成或者存在之后才进行 cacu的命令动作。 -
文件的时间戳:make 命令执行的时候会根据文件的时间戳判定是否执行相关的命令,并且执行依赖于此项的规则。例:对 main.c 文件进行修改后保存,文件的生成日期就发生了改变,再次调用make 命令编译的时候,就会只编译main.c, 并且执行规则 cacu, 重新链接程序。 -
执行的规则:在调用 make 命令编译的时候,make 程序会查找Makefile 文件中的第 1个规则,分析并执行相关的动作。例子中的第 1个规则为cacu, 所以 make 程序执行 cacu 规则。由于其依赖项包含 5 个,第 1个为add_int.o, 分析其依赖项,当 add/add_int.c add.h 存在的时候,执行如下命令动作:gcc -c -o add/add_int.o add/add_int.c -
模式匹配:在上面的Makefile中,main.o规则的书写方式如下:
简单的方法:
这种方法的规则 main.o 中依赖项中的 “%o:% c” 的作用是将 TARGET 域的**.0** 的扩展名替换为**.c**, 即将 main.o 替换为main.c。而命令行的$<表示依赖项的结果,即main.c; $@ 表示 TARGET 域的名称,即m ain.o。
Makefile中使用变量
上面生成cacu的规则如下:
cacu:add_int.o add_float.o sub_int.o sub_float.o main.o
gcc -o add/add_int.o add/add_float.o\
sub/sub_int.o sub/sub_float.o main.o
生成 cacu 的时候,多次使用同一 组**.o**目标文件:在 cacu 规则的依赖项中出现一 次,在生成 cacu 执行文件的时候又出现一 次。直接使用文件名进行书写的方法不仅书写起来麻烦,而且进行增加或者删除文件时容易遗忘。例如增加一 个 mul.c 文件,需要修改依赖项和命令行两个部分。
①.Makefile中的用户自定义变量
使用Make进行规则定义的时候,用户可以定义自己的变量,称为用户自定义变量。例:可以用变量来表示上述的文件名,定义OBJS变量表示目标文件:
OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o
调用OBJS的时候加上$,并且将变量的名称用括号括起来,例:使用gcc的默认规则进行编译,cacu规则可以采用如下形式:
cacu:
gcc -o cacu $ (OBJS)
当用CCB变量表示gcc,用CFLAGS表示编译的选项,RM表示rm -f,TARGET表示最终的生成目标cacu:
CC = gcc
CFLAGS = -Isub -Iadd
TARGET = cacu
RM = rm -f
之前冗余的Makefile可以简化如下方式:
CC = gcc
CFLAGS = -Iadd -Isub -02
OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o
TARGET = cacu
RM = rm - f
$(TARGET):(OBJS)
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
$(OBJS):%.o:%.c
$(CC) -c $(CFLAGS) $< -o $@
clean:
-$(RM) $(TARGET) $(OBJS)
执行情况如下:
make查找到第一个执行的规则为生成cacu,但是main.o等5个文件不存在,make按照默认的规则生成main.o等5个目标文件。
②.Makefile中的预定义变量
在Makefile中有一些已经定义的变量,用户可以直接使用,不用定义,如下图:
在Makefile中常用变量CC来编译器,其默认值为cc,即使用cc命令来进行C语言程序的编译;在进行程序删除的时候经常使用命令RM,它的默认值为rm -f 。
另外还有CFLAGS等默认值是调用编译器时的选项默认配置,例如修改后的Makefile生成main.o时,没有指定编译选项,make程序自动调用了文件中定义的CFLAGS选项 -Iadd-Isub-02来增加头文件的搜索路径,在所有的目标文件中都采用了此设置。经过简化后,之前的Makefile可以采用如下形式:
CFLAGS = -Iadd -Isub -02
OBJS = add/add_int.o add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o
TARGET = cacu
$(TARGET):$(OBJS)
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
clean:
-$(RM) $(TARGET) $(OBJS)
上面的Make中clean目标中的**$(RM) $(TARGET) $(OBJS)之前的符合"-**"表示当操作失败时不会报错,命令进行执行。如果当前目录不存在cacu时,它会继续删除其他的目标文件。例如:下面的clean规则在没有cacu文件时会报错:
clean:
rm $(TARGET)
rm $(OBJS)
执行clean:
make clean
Makefile中的自动变量
Makefile中的变量除了用户自定义变量和预定义变量外,还有一类自动变量。Makefile中的编译语句中经常会出现目标文件和依赖文件,自动变量代表这些目标文件和依赖文件。下表中是一些常见的自动变量。
CFLAGS = -Iadd -Isub -02
OBJS = add/add_int.o add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o
TARGET = cacu
$(TARGET):$(OBJS)
$(CC) $@ -o $^ $(CFLAGS)
$(OBJS):%.o:%.c
$(CC) $< -c $(CFLAGS) -o $@
clean:
-$(RM) $(TARGET) $(OBJS)
重新编写后的Makefile中,生成TARGET规则的编译选项使用$@ 表示依赖项中的文件名称,使用$< 表示目标文件的名称。例:
$(TARGET):$(OBJS)
$(CC) $@ -o $^ $(CFLAGS)
与
$(TARGET):$(OBJS)
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
是一致的。
搜索路径
在大的系统中,通常存在很多目录,手动添加目录的方法不仅十分笨拙而且容易造成 错误。Make的目录搜索功能提供了一个解决此问题的方法,指定需要搜索的目录,make会自动找到指定文件的目录并添加到文件上,VPATH变量可以实现此目的。VPATH变量的使用方法如下:
VPATH=path1:path2:...
VPATH右边是冒号(:)分隔的路径名称,例如:
VPATH = add:sub(加入add和sub搜索路径)
add_int.o:%o:%c
$(CC) -c -o $@ $<
Make的搜索路径包含add和sub目录。add_int.o规则自动扩展如下代码:
add_int.o:add/add_int.c
cc -c -o add_int.o add/add_int.c
将路径名去掉以后可以重新编译程序,但是会发现目标文件都放到了当前的目录下,会对文件的规范造成危害。可以将输出的目标文件放到同一个目录下来解决此问题,重新书写上面的Makefile代码:
CFLAGS = -Iadd -Isub -02
OBJSDIR = objs
VPATH=add: sub:.
OBJS = add_int.o add float.a sub_int.o sub float.a main.o
TARGET= cacu
$(TARGET):$(OBJSDIR) $(OBJS)
$(CC) -o $(TARGET) $(OBJSDIR)
这样,目标文件都放到objs目录下,只有最终执行文件cacu放在当前目录,执行make:
make cacu
编译目标文件时会自动地加上路径名,例如add_int.o所依赖的文件add_int.c自动变成add/add_int.c;并且当objs不存在时会创建此目录。
自动推导规则
-
make编译扩展名为**.c的C语言文件,源文件的编译规则不用明确给出。这是因为make进行编译的时候会使用一个默认的编译规则按照默认规则完成对.c文件的编译,生成对应的.o文件。它执行命令cc -c来编译.c**源文件。 -
在Makefile中只要给出需要重建的目标文件名( 一 个**.o** 文件),make会自动为这个**.o** 文件寻找合适的依赖文件(对应的.c文件),并且使用默认的命令来构建这个目标文件。 -
对于上边的例子,默认规则是使用命令cc -c main.c-o main.o来创建文件main.o。对 一个目标文件是" 文件名.o", 依赖文件是"文件名.c " 的规则,可以省略其编译规则的命令行,由make命令决定如何使用编译命令和选项。此默认规则称为make的隐含规则。
这样,在书写Makefile时,就可以省略描述**.c文件和.o依赖关系的规则,而只需要给出那些特定的规则描述(.o目标所需要的.h**文件)。因此上面的例子可以使用更加简单的方式书写,Makefile文件的内容如下:
CFLAGS = -Iadd -Isub -02
VPATH = add:sub
OBJS = add_int.o add_float.o sub_int.o sub_float.o main.o
TARGET = cacu
$(TARGET):$(OBJS)
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
clean:
-$(RM) $(TARGET)
-$(RM) $(OBJS)
在此Makefile中,不用指定OBJS的规则,make自动会安值隐含规则形成一个规则来生成目标文件。
递归make
当有多人在多个目录下进行程序开发,并且每个人负责 一个模块,而文件在相对独立的目录中,这时由同一 个Makefile维护代码的编译就会十分鳖脚,因为他们对自己目录下的文件增减都要修改此Makefile, 这通常会造成项目的维护问题。
①.递归调用的方式
Make命令有递归调用的作用,它可以递归调用每个字目录的Makefile。例:在当前目录下有一个Makefile,而目录add和sub及主控文件main.o由不同的人进行维护,可以用如下编译add的文件:
add:
cd add && $(MAKE)
等价于:
add:
$(MAKE) -C add
先进入子目录下add中,然后执行make命令。
②.总控Makefile
调用“$(MAKE) -C”的Makefile叫总控Makefile。如果总控Makefile中的一些变量需要传递下层1的Makefile,可以使用export命令,例:需要向下层的Makefile传递目标文件的导出路径:
export OBJSDIR = ./objs
例如上面地文件布局,需要在add和sub目录下分别编译,总控Makefile代码如下:
CC = gcc
CFLAGS = -02
export = OBJSDIR = ${shell pwd}/objs
$(TARGET):$(OBJSDIR) main.o
$(MAKE) -C add
$(MAKE) -C sub
$(CC) -o $(TARGET) $(OBJSDIR)
-
CC 编译器变量由总控 Makefile 统一 指定,下层的 Makefile 直接调用即可。生成的目标文件都放到**/objs** 目录中,export 了一 个变量OBJSDIR 。其中的**${shell pwd}** 是执行一 个shell 命令pwd 获得总控 Makefile 的当前目录。 -
生成 cacu 的规则是先建立目标文件的存放目录,再编译当前目录下的main.c 为 main.o目标文件。 -
在命令中,递归调用 add 和 sub 目录下的Makefile 生成目标文件放到目标文件存放路径中,最后的命令将目标文件全部编译生成执行文件 cacu 。
③.子目录Makefile的编写
add目录下的Makefile如下:
OBJS = add_int.o add_float.o
all:$(OBJS)
$(OBJS):%.o:%.c
$(CC) -c $< -o $(OBJSDIR)/$@ $(CFLAGS)
clean:
$(RM) $(OBJS)
这个 Makefile 很简单,编译 add 目录中的两个 C 文件,并将生成的目标文件按照总控Makefile 传入的目标文件存放路径放置。 sub 目录下的Makefile 与 add 目录下的一 致,也是将生成的目标文件放到总控 M akefile指定的路径中。
OBJS = sub_int.o sub_float.o
all:$(OBJS)
$(OBJS):%.o%.c
$(CC) -c $< -o $(OBJSDIR)/$@ $(CFLAGS)
clean:
$(RM) $(OBJS)
Makefile中的函数
①.获取匹配模式的文件名wildcard
函数功能:查找当前目录下所有符合模式PATTERN的文件名,其返回值是以空格分割的、当前目录下的所有符合模式PATTERN的文件名列表,其原型如下:
$(wildcard PATTERN)
例如:如下模式返回当前目录下所有扩展名为.c的文件列表:
$(wildcard *.c)
②.模板替换函数patsubst
函数功能:查找字符串test中按照空格分开的单词,将符合模式pattern的字符串替换成replacement。pattern中的模式可以使用通配符,%代表0到n个字符,当pattern和replacement中都有%时,符合条件的字符将被replacement中的替换。函数返回值是替换后的新字符串,其原型如下:
$(patsubst pattern,replacement,text)
返回值:例如需要将C文件替换会.o的目标文件可以使用如下模式:
$(patsubst %.c,%.o,add.c)
上面模式将add.c字符串作为输入,当扩展名为.c时符合模式%.c,其中%在这里代表add,替换为add.o,并作为输出字符串:
$(patsubst %.c,%.o, $(wildcard *.c))
输出的字符串将扩展名**.c的文件替换成扩展名为.o**的文件列表。
③.循环函数foreach
函数原型:
$(foreach VAR,LIST,TEXT)
函数功能:将LIST字符串中一个空格分割的单词,先传给变量VAR,然后执行TEXT表达式,TEXT表达式,TEXT表达式处理结束后输出。其返回值是空格分割表达式TEXT的计算结果。
例:对于add和sub的两个目录,设置DIRS为"add sub ./"包含目录add、sub和当前目录。表达式**$(wildcard(dir)/*.c)**,可以取出目录add和sub及当前目录中的所有扩展名为.c的C语言源文件。
DIRS = sub add ./
FILES = $(foreach dir,$(DIRS),$(wildcard $(dir)
利用上面几个函数对Makefile文件进行重新编写,使新的Makefile可以自动更新各个目录下的C语言源文件:
CC = gcc
CFLAGS = -02 -Iadd -Isub
TARGET = cacu
DIRS = sub add .
FILES = $(foreach dir,$(DIRS),$(wildcard $(dir)
编译:
用GDB调试程序
使程序能够正常运行,跟踪代码、调试漏洞是不可缺少的。Linux中包含一个很强大的调试工具GDB,可以用它来调试C和C++程序。GDB功能如下:
-
在程序中设置断点,当程序运行到断点处暂停。 -
显示变量的值,可以打印或者监视某个变量,将变量的值显示出来。 -
单步执行,GDB 允许用户单步执行程序,可以跟踪进入函数和从函数中退出。 -
运行时修改变量的值, GDB 允许在调试状态下修改变量的值,此功能在测试程序的时候是十分有用的。 -
路径跟踪, GDB 可以将代码的路径打印出来,方便用户跟踪代码。 -
线程切换,在调试多线程的时候,此种功能是必须的。
GDB的功能远不止这些,例如可以显示程序的汇编代码、打印内存的值。
编译可调试程序
GDB是一套字符界面的程序集,可以使用gdb加载要调试的程序。例如输入gdb显示GDB版权:
按q,退出GDB。
使用GDB调试,输入**-g**。例如:
#include<stdio.h>
#include<stdlib.h>
static int sum(int value);
struct inout
{
int value;
int result;
};
int main(int argc,char *argv[])
{
struct inout *io = (struct inout*)malloc(sizeof(struct inout));
if(NULL == io)
{
printf("申请失败!\n");
return -1;
}
if(argc !=2)
{
printf("参数输入错误!\n");
return -1;
}
io->value = *argv[1]-'0';
io->result = sum(io->value);
printf("你输入的值为: %d,计算结果为: %d\n",io->value,io->result);
return 0;
}
static int sum(int value)
{
int result = 0;
int i=0;
for(i=0;i<value;i++)
result +=i;
return result;
}
编译:
gcc -o test gdb-01.c -g
生成test可执行文件,运行:
./test
使用GDB调试程序
利用GBD调试test,查找test计算错误的原因:
①.加载程序
②.设置输入参数
GBD中向可执行文件输入参数的命令格式为"set args 参数值1 参数值2 …"。例如下面的set args 3表示向可执行文件输入参数设为3,即传给test程序的值为3:
③.打印代码内容
命令list用于列出可执行文件对应源文件的代码,命令格式为"list 开始的行号"。例如:下面的list 1
④.设置断点
b命令在某一行设置断点,程序运行到断点的位置会中断,等待的下步操作指令。
⑤.运行程序
命令run运行程序:
⑥.显示变量
程序运行到设置断点时的位置,程序会中断运行等待进一步的指令,这时可以进行一系列的操作,其中,命令display可用用来显示变量的值:
通过上面的跟踪,已经可用判断问题出在for(i=0;i<value;i++);可以修改为for(i=1;i<=value;i++),或者修改为resu+=i为result+=(i+1)。
⑦.修改变量
命令set用于修改变量的值。
⑧.退出GDB
GDB常用命令
GDB的常用命令主要包含信息获取、断点设置、运行控制、程序加载等常用命令,这些函数可以进行调试时的程序控制、程序的参数设置等。
①.执行程序
用GDB 执行程序可以使用gdb program 的方式,program是程序的程序名。当然,此程序编译的时候要使用 -g 选项。假如在启动GDB 的时候没有选择程序名称,可以在GDB启动后使用 file program 的方法启动。例如:
file test
如果运行准备好的程序,可以使用run命令,在它后面是传递程序的函数,例如:
注:如果使用不带参数的run命令,GDB就再次使用前一条run命令的参数。
②.参数设置和显示
使用set args命令来设置发送给程序的参数,使用show args命令就可以查看器默认的参。
如果按Enter键,GDB默认执行上一行命令,例如上面的例子中在执行命令show args后,按Enter键会接着执行show args命令。
③.列文件清单
打印文件代码命令是list 简写l,命令格式为:
list line1,line2
打印出从line1到line2之间的代码,如果不输入参数,则从当前行开始打印。例如:打印从第2行到第5行之间的代码:
④.打印数据
打印变量或者表达式的值可以使用print命令,简写为p,使用方式如下:
print var
print打印表达式的值: print计算函数调用的返回值,例如:打印调用函数sum()对3求和:
print打印结构中各个成员的值,例如打印上面代码中io结构中各个成员的值: GDB的系统中,之前打印的历史值保存在全局变量中。如$9中保存了结构io的值,用print可以打印历史值,例: 利用print可以打印构造数组的值,给出数组的指针头并且设定要打印的结构数量,print命令会依次打印各值。打印构造数组的格式为: 例:*io是结构ioout的头,要打印从io开始的两个数据结构(当然最后一个是非法的,这里只是一个例子):
⑤.断点
设置断点命令是:break,简写:有如下三种形式,注意GDB的停止位置都是在执行程序之前:
-
break行号:程序停止在设定的行之前。 -
break函数名称:程序停止在设定的函数之前。 -
break行号或者函数if条件:这是一个条件断点设置命令,如果条件为真,则程序在到达指定行或函数时停止。
设置断点。如果程序由很多的文件构成,在设置断点时要指定文件名:
设置条件断点,可以利用break if命令,在调试循环代码段时这样设置比较有用,省略了大段的手动调试。例如:在sum()函数中,当i为2时要设置断点,如下:
⑥显示断点信息
显示当前断点的信息:info break,例如:
信息分为6类:
- Num是断点的编号
- Type 是信息的类型
- Disp是描述
- Enb是断点是否有效
- Address是断点在内存中的地址
- What是对断点在源文件中位置的描述
第3个断点的停止条件当i==2时,已经命中1次。
⑦.删除某个断点
删除某个指定的断点使用命令:delete,命令格式为"delete breakpoint 断点编号"。 注:如果不带参数,将删除所有断点。
⑧.禁止断点
禁止某个断点使用命令disable,将某个断点禁止后,GDB进行调试的时候,在断点处程序不再中断。命令的格式为"disable breakpoint 断点编号"。例如:
⑨.允许断点
允许某个断点使用命令enable。这个命令将禁止的断点重新启用,GDB会在启用的断点处重新中断。命令的格式为"enable breakpoint 断点编号"。例如:
⑨.清除断点
一次性的清除某行处的所有断点使用命令clear,这个命令将清除某行的所有断点信息。此时不能使用enable命令重新允许断点生效。例如:下面命令clear 29,将清除在源代码文件中第29行的断点:
变量类型检测
在调试过程中有需要查看变量类型的情况,此列命令有whatis、ptype等。
-
打印数组或者变量的类型 whatis: 要查看程序中某个变量的类型,可以使用命令wahtis 。whatis命令的格式为 “whatis 变量名”,其中的变量名是要查看的变量。例如查看 io 和argc 的变量类型,io 为struct inout 类型,argc 为int类型。 -
结构的详细定义 ptype: 使用 whatis命令查看变量的类型时,只能获得变量的类型名称,不能得到类型的详细信息。如果想要查看变量的详细信息,需要使用命令ptype。例如查看io 的类型,最后的*****表明io 是一 个指向struct inout 类型的指针。
单步调试
在调试程序的时候经常遇到要单步跟踪的情况,并在适当的时候进入函数体的内部继续跟踪。 GDB 的next命令和 step 命令提供了这种功能。
- next命令是单步跟踪的命令,简写为 n;
- step 是可以进入函数体的命令,简写为s。
- 如果已经进入某个函数,想退出函数的运行,返回到调用的函数中,要使用命令 finish。
例如在代码的第28行设置断点,跟踪程序,在第 34 行进入sum ()函数的内部继续跟踪。
设置监测点
命令display可以显示某个变量的值,在结束或者断点的时候,将设置变量的值显示出来。当然是否显示还要看变量的作用域, display 只显示作用域内变量的值。例如,将io 和 sum 中的result设置为显示,设置断点在第27行、第 29行和第 38 行,进行调试的情况如下:
调用路径
backtrace命令可以打印函数的调用路径,提供向前跟踪功能,此命令对跟踪函数很有用处。backtrace命令打印一 个顺序列表,函数从最近到最远的调用过程,包含调用函数和其中的参数。其简写为bt。例如在第38行设置断点后打印调用过程:
信息
info命令可以获得当前命令的信息,例如获得断点的情况,参数的设置情况等。
多线程
- 多线程是现代程序中经常采用的编程方法,而多线程由于执行过程中的调度随机性,不好调试。
- 多线程调试主要有两点:先获得线程的ID号,然后转到该线程进行调试。
- info thread命令列出当前进程中的线程号,其中最前面的为调试用得ID。
- 用thread id进入需要调试的线程。
汇编disassemble
disassmble命令打印指定函数的汇编代码,例如sum的汇编d代码如下:
GBD的帮助信息
使用help命令获取帮助: 对命令c等的疑问可以使用命令help获得解释: help 解释 c 命令用千继续执行程序,并且可以设置执行的行数。其实c 命令是continue的简写。
其他的GBD
除了基于命令行的GDB调试程序,在 Linux 下还有很多其他基于GDB 的程序,例如xxgdb、insight 等。
1.xxgdb
xxgdb 对命令行的GDB 进行了简单的包装,将 GDB 的输入、命令、输出分为了几个窗口,并且将命令用多个按钮表示,方便使用。
2.Emacs
Emacs集成了GDB的调试,可以用下面的命令启动GDB。
M-x gdb
在Emacs中有一种多窗口调试模式(gdb-many-windows),可以把窗口划分为5个窗格,同时显示gdb命令窗口、当前局部变量、程序文本、调用栈和断点。 Emacs对gdb的命令和快捷键做了绑定。对于常用的命令,输入快捷键比较方便。例如,Ctrl+C和Ctrl+N是nextline, Ctrl+C和Ctrl+S是stepin, 在调试过程中用得最多的快捷键就是这两个。
|