Linux项目准备工作

2021/10/20 7:09:35

本文主要是介绍Linux项目准备工作,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Makefile 编写

程序的编译和链接
使用C、C++编写可执行程序,首先要把源文件编译成中间代码文件,Linux下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。

一个项目,拥有成百上千的源程序文件,编译链接这些源文件都是由规则的,Makefile闪亮登场!Makefile确定整个工程的编译规则,只需要一个make命令,就可以实现“自动化编译”。make是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。

Make 工作原理
通常在一个项目里,我们的规则是:
1)如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
2)如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
3)如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。

只要我们的Makefile写得够好,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。

Makefile的规则:
target … : prerequisites …
command

target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,暂不叙述。

prerequisites就是,要生成那个target所需要的文件或是目标。

command也就是make需要执行的命令。(任意的Shell命令)

这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。

例子:

hello_demo : hellospeak.o speak.o                                           
        gcc -o hello_demo hellospeak.o speak.o                               

hellospeak.o : hellospeak.c speak.h                                           
        gcc -c hellospeak.c                                                
speak.o : speak.c  speak.h                                                  
        gcc -c speak.c                                                    
clean :                                                                  
        rm hello_demo speak.o  hellospeak.o                         

Make 工作流程

1、make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“hello_demo”这个文件,并把这个文件作为最终的目标文件。
3、如果hello_demo文件不存在,或是hello_demo所依赖的后面的 .o 文件的文件修改时间要比hello_demo这个文件新,那么,他就会执行后面所定义的命令来生成hello_demo这个文件。
4、如果hello_demo所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(像不像堆栈过程?)
5、当然,我们的C文件和H文件都存在,于是make会生成 .o 文件,然后再用 .o 文件生命make的终极任务,也就是执行文件hello_demo了。

Make 变量
一个Makefile里我们发现经常会由重复的内容,如上面范例中的前两行中的
hellospeak.o speak.o

如果我们的工程需要加入一个新的[.o]文件,那么我们好几个地方都需要修改原来的makefile。当然,我们的makefile并不复杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,完全可以理解成C语言中的宏

变量定义: 变量名 = 值 ##使用shell script 的语法
如:
objects = hellospeak.o speak.o

我们上面的makefile 就可以变成:

objects = hellospeak.o speak.o                                               
hello_demo : $(objects)                                                   
        gcc -o hello_demo $(objects)                                        

hellospeak.o : hellospeak.c speak.h                                           
        gcc -c hellospeak.c                                                
speak.o : speak.c  speak.h                                                  
        gcc -c speak.c                                                    
clean :                                                                  
        -rm hello_demo  $(objects)  

更省事的方式,让Make 自动推导

make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。

只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个hello.o,那么hello.c,就会是hello.o的依赖文件。并且 gcc -c hello.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的新的makefile又出炉了。

objects = hellospeak.o speak.o                                                   
hello_demo : $(objects)                                                        
        gcc -o hello_demo $(objects)                                            
                                                                           
$(objects):speak.h                                                             
#hellospeak.o : hellospeak.c speak.h                                              
#       gcc -c hellospeak.c                                                    
#speak.o : speak.c  speak.h                                                     
#       gcc -c speak.c                                                        
                                                                           
.PHONY : clean                                                              
clean :                                                                      
        -rm hello_demo  $(objects)                      

CMake,让Makefile 来得更猛男一点

你或许听过好几种 Make 工具,例如 GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。
CMake就是针对上面问题所设计的工具:它首先允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。一些使用 CMake 作为项目架构系统的知名开源项目有 VTK、ITK、KDE、OpenCV、OSG 等 。

安装cmake: sudo apt-get install cmake

在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:
1.编写 CMake 配置文件 CMakeLists.txt 。
2.执行命令 cmake PATH 生成 Makefile。其中, PATH 是 CMakeLists.txt 所在的目录。
3.使用 make 命令进行编译。

#CMakeLists.txt 基本玩法
#CMake 最低版本要求
cmake_minimum_required(VERSION 2.8)

#项目信息
project(HelloSpeak)

#指定生成的目标
add_executable(CmakeHelloSpeak speak.c hellospeak.c)

gdb调试 与 coredump

gdb是GNU 发布的一个强大的程序调试工具,也是Linux 程序员不可或缺的一大利器。
本章我们将给出gdb 常用的命令的操作说明。
启动gdb
使用gdb的前提:
$ gcc -g hello.c -o hello

启动gdb 调试:
$ gdb hello

注意:
如果系统没有安装gdb 的同学,可以体验使用源码安装的方式来安装:

  1. wget http://ftp.gnu.org/gnu/gdb/gdb-8.1.1.tar.gz
  2. tar -zxvf gdb-8.1.1.tar.gz
  3. cd gdb-8.1.1
  4. ./configure
  5. make
  6. make install

然后就可以进入命令行操作,和 Shell 一样,gdb 支持命令补全。输入几个字母,按Tab键,gdb 会补全命令。按两次Tab ,会提示所有可能的命令。
另外,其还支持命令缩写,如, h 代表 help ,
常用命令如下:

获取帮组命令 help
gdb将命令分为12个大类,使用命令: “help 子类别”可以查看没类下面的详细帮助。

如: help breakpoints

设置断点
break 命令用于设置断点,命令接受行号或者函数名作为参数。
在这里插入图片描述
break也可设置条件断点:
如: break sayHello if count<1
使用info break 可以查看断点

使用 clear 可以清除断点

运行程序和单步执行
设置完断点后,就可以运行调试程序了,使用run命令(缩写为 r)运行程序至断点。
在这里插入图片描述
此时,程序中断,gdb 等待用户发出下一步操作的指令。设用next 命令(缩写为 n)单步执行程序。也可以指定一个数字。下面这条命令让gdb 连续执行两行,然后停下。

如果想继续运行,可以使用continue 命令(缩写为 c )指导 gdb 继续运行程序,直至遇到下一个断点。
如果想继续单步执行,可以继续使用 next,也是以使用 step (缩写为 s), step 和 next 的最大的区别在于,step 遇到函数是会进入到内部,而next 不会进入内部.

监视变量
调试程序最基本的需求就是监视变量的值,可以使用 print 命令(缩写为 p) 显示指定变量的值。
在这里插入图片描述
如果要时刻监视某个变量的值,那么每次使用 print 就不方便。比较人性化的是,gdb 提供了watch 命令,用于设置另一种断点:“观察点”。
用法是: watch 变量名或表达式作为参数,一但值发生变化,就停下来。

临时修改变量
当某些特殊情况下,我们想让程序进入一些特殊的流程时,gdb允许用户在程序运行时改变变量的值,通过 set var 命令实现这一点。
在这里插入图片描述
查看堆栈情况

每次程序调用一个函数,函数的地址、参数、函数内部变量都会被压入“栈”(Stack) 中,运行时堆栈信息对于程序员非常重要,使用 “bt”命令可以看到当前运行时栈的情况。
在这里插入图片描述
退出 gdb
调试完毕,使用quit命令(缩写为q) 退出 gdb程序。

Coredump 调试

coredump是什么?

程序异常退出时,会产生一个core文件,该文件记录了程序运行时的内存,
寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们
可以理解为是程序工作当前状态存储生成的一个文件,通过工具分析这个文
件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题
所在并进行及时解决。

前期设置

  1. 设置core文件生成的目录,其中%e表示程序文件名,%p表示进程ID,
    否则会在程序的当前目录生成dore文件;
    echo /data/coredump/core.%e.%p >/proc/sys/kernel/core_pattern
    注意: data文件需要自行创建

  2. 当前执行程序的用户对core目录有写权限且有足够的空间存储core文件;

  3. 生成不受限制的core文件;
    ulimit -c unlimited

什么情况下会导致程序异常退出
非法指针的访问,堆栈溢出

如何调试

  1. 编译的时候添加-g选项,增加调试信息
  2. gdb program core_file
bt或者where查看调用栈信息
  
如果你要查看某一层的信息,你需要切换当前的栈,一般来说,程序停止时,最顶层的栈就是当前栈,如果你要查看栈下面层的详细信息,首先要做的是切换当前栈。

frame <n> 
f <n> 
    n是一个从0开始的整数,是栈中的层编号。比如:frame 0,表示栈顶,frame 1,表示栈的第二层。

up <n>
    表示向栈的上面移动n层,可以不打n,表示向上移动一层。 
    
down <n> 
    表示向栈的下面移动n层,可以不打n,表示向下移动一层。 
上面的命令,都会打印出移动到的栈层的信息。如果你不想让其打出信息。你可以使用这三个命令:

        select-frame <n> 对应于 frame 命令。
        up-silently <n> 对应于 up 命令。
        down-silently <n> 对应于 down 命令。

查看当前栈层的信息,你可以用以下GDB命令:

frame 或 f 
    会打印出这些信息:栈的层编号,当前的函数名,函数参数值,函数所在文件及行号,函数执行到的语句。

info frame 
info f 
    这个命令会打印出更为详细的当前栈层的信息,只不过,大多数都是运行时的内内地址。比如:函数地址,调用函数的地址,被调用函数的地址,目前的函数是由什么样的程序语言写成的、函数参数地址及值、局部变量的地址等等。如:

在这里插入图片描述

info args
打印出当前函数的参数名及其值。

 info locals
    打印出当前函数中所有局部变量及其值。

另类的方式?打印堆栈

//信号钩子函数,获取栈信息,然后在日志中打印

void handle_segv(int signum)

{

void *array[100];

size_t size;

char **strings;

size_t i;

signal(signum, SIG_DFL); /* 还原默认的信号处理handler */

size = backtrace (array, 100);
strings = (char **)backtrace_symbols (array, size);

fprintf(stderr,"Launcher received SIG: %d Stack trace:\n", signum);
for (i = 0; i < size; i++)
{
    fprintf(stderr,"%d %s \n",i,strings[i]);
}

free (strings);

}

在main 函数中加入:
signal(SIGSEGV, handle_segv); // SIGSEGV 11 Core Invalid memory reference
signal(SIGABRT, handle_segv); // SIGABRT 6 Core Abort signal from

注意:编译时一定要带上 -g 选项

使用addr2line命令检测:
addr2line -a 0x4007fa -e demo_log

内存检测Valgrind

Valgrind是运行在Linux上一套基于仿真技术的程序调试和分析工具,作者是获得过Google-O’Reilly开源大奖的Julian Seward,它包含一个内核──一个软件合成的CPU,和一系列的小工具,每个工具都可以完成一项任务──调试,分析,或测试等,内存检测,我们可以使用它的工具:Memcheck。

Valgrind 安装
方法 1. valgrind官网:http://valgrind.org下载

方法 2. Ubuntu sudo apt-get install valgrind

Memcheck检测范围

用来检测c/c++程序中出现的内存问题,所有对内存的读写都会被检测到,一切对malloc()/free()/new/delete的调用都会被捕获。所以,它能检测以下问题:

  1. 对未初始化内存的使用;

  2. 读/写释放后的内存块;

  3. 读/写超出malloc等分配的动态内存范围;

  4. 读/写不适当的栈中内存块;

  5. 内存泄漏,指向一块内存的指针永远丢失;

  6. 不正确的malloc/free或new/delete匹配;

  7. memcpy()相关函数中的dst和src指针重叠问题。

Memcheck 检查步骤及注意事项

1.在编译程序的时候打开调试模式(gcc编译器的-g选项),以便显示行号,编译时去掉-O1 -O2等优化选项;检查的是C++程序的时候,考虑加上选项: -fno-inline ,这样它函数调用链会很清晰

  1. 执行:valgrind --tool=memcheck --leak-check=full --log-file=./log.txt ./YourProgram

3.程序运行结束,查看 log.txt 中的结果。

结果分析
Valgrind(memcheck)包含这7类错误,黑体为一般的错误提示:

1.illegal read/illegal write errors 非法读取/非法写入错误

2.use of uninitialised values 使用未初始化的区域

3.use of uninitialised or unaddressable values in system calls 系统调用时使用了未初始化或不可寻址的地址

4.illegal frees 非法的释放

5.when a heap block is freed with an inappropriate deallocation function 分配和释放函数不匹配

6.overlapping source and destination blocks 源和目的内存块重叠

7.memory leak detection 内存泄漏检测

7.1 Still reachable 
内存指针还在还有机会使用或者释放,指针指向的动态内存还没有被释放就退出了

7.2 Definitely lost 
确定的内存泄露,已经不能够访问这块内存

7.3 Indirectly lost 
指向该内存的指针都位于内存泄露处

7.4 Possibly lost 
可能的内存泄露,仍然存在某个指针能够访问某块内存,但该指针指向的已经不是该内存首位置

7.5 Suppressed  某些库产生的错误不予以提示,这些错误会被统计到suppressed项目

gtest单元测试

gtest是Google的一套用于编写C++测试的框架,可以运行在很多平台上(包括Linux、Mac OS X、Windows、Cygwin等等)。基于xUnit架构。支持很多好用的特性,包括自动识别测试、丰富的断言、断言自定义、死亡测试、非终止的失败、生成XML报告等等。

1.gtest的优点

好的测试应该有下面的这些特点,我们看看GTest是如何满足要求的。
1.测试应该是独立的、可重复的。一个测试的结果不应该作为另一个测试的前提。GTest中每个测试运行在独立的对象中。如果某个测试失败了,可以单独地调试它。

2.测试应该是有清晰的结构的。GTest的测试有很好的组织结构,易于维护。

3.测试应该是可移植和可复用的。有很多代码是不依赖平台的,因此它们的测试也需要不依赖于平台。GTest可以在多种操作系统、多种编译器下工作,有很好的可移植性。

4.测试失败时,应该给出尽可能详尽的信息。GTest在遇到失败时并不停止接下来的测试,而且还可以选择使用非终止的失败来继续执行当前的测试。这样一次可以测试尽可能多的问题。

5.测试框架应该避免让开发者维护测试框架相关的东西。GTest可以自动识别定义的全部测试,你不需要一一列举它们。
测试应该够快。GTest在满足测试独立的前提下,允许你复用共享数据,它们只需创建一次。

6.GTest采用的是xUnit架构,你会发现和JUnit、PyUnit很类似,所以上手非常快。

2.搭建测试框架
gtest下载地址: https://github.com/google/googletest
下载方法是:git clone https://github.com/google/googletest.git
注意:需要手打才可以,复制的不行;
安装方法是:

$ cd googletest

注意:如果在make 过程中报错,可在CMakeLists.txt 中增加如下行,再执行下面的命令: SET(CMAKE_CXX_FLAGS “-std=c++11”)
$ cmake .
$ make
然后在lib目录下会生成:libgmock.a libgmock_main.a libgtest.a libgtest_main.a
最后我们再sudo make install。

3.测试Demo

第一步:假设我们的实现两个函数:

#include "sample1.h"

// Returns n! (the factorial of n).  For negative n, n! is defined to be 1.
int Factorial(int n) {
  int result = 1;
  for (int i = 1; i <= n; i++) {
    result *= i;
  }

  return result;
}

// Returns true iff n is a prime number.
bool IsPrime(int n) {
  // Trivial case 1: small numbers
  if (n <= 1) return false;

  // Trivial case 2: even numbers
  if (n % 2 == 0) return n == 2;

  // Now, we have that n is odd and n >= 3.

  // Try to divide n by every odd number i, starting from 3
  for (int i = 3; ; i += 2) {
    // We only have to try i up to the square root of n
    if (i > n/i) break;

    // Now, we have i <= n/i < n.
    // If n is divisible by i, n is not prime.
        if (n % i == 0) return false;
  }

  // n has no integer factor in the range (1, n), and thus is prime.
  return true;
}

这两个函数定义在sample1.cc文件里,函数申明在sample1.h里:

#ifndef GTEST_SAMPLES_SAMPLE1_H_
#define GTEST_SAMPLES_SAMPLE1_H_

// Returns n! (the factorial of n).  For negative n, n! is defined to be 1.
int Factorial(int n);

// Returns true iff n is a prime number.
bool IsPrime(int n);

#endif 

第二步:现在我们就是要测试Factorial和IsPrime两个函数是否正确,好了开始写我们的测试用例把。新建一个文件,命名为sample_unittest.cc,代码如下:

#include <limits.h>
#include "sample1.h"
#include "gtest/gtest.h"
namespace {

TEST(FactorialTest, Negative) {
    // This test is named "Negative", and belongs to the "FactorialTest"
    // test case.
    EXPECT_EQ(1, Factorial(-5));
    EXPECT_EQ(1, Factorial(-1));
    EXPECT_GT(Factorial(-10), 0);
}

TEST(FactorialTest, Zero) {
   EXPECT_EQ(1, Factorial(0));
}

TEST(FactorialTest, Positive) {
    EXPECT_EQ(1, Factorial(1));
    EXPECT_EQ(2, Factorial(2));
    EXPECT_EQ(6, Factorial(3));
    EXPECT_EQ(40320, Factorial(8));
}

// Tests IsPrime()
TEST(IsPrimeTest, Negative) {
  EXPECT_FALSE(IsPrime(-1));
  EXPECT_FALSE(IsPrime(-2));
  EXPECT_FALSE(IsPrime(INT_MIN));
}

TEST(IsPrimeTest, Trivial) {
  EXPECT_FALSE(IsPrime(0));
  EXPECT_FALSE(IsPrime(1));
  EXPECT_TRUE(IsPrime(2));
  EXPECT_TRUE(IsPrime(3));
}

TEST(IsPrimeTest, Positive) {
  EXPECT_FALSE(IsPrime(4));
  EXPECT_TRUE(IsPrime(5));
  EXPECT_FALSE(IsPrime(6));
  EXPECT_TRUE(IsPrime(23));
}
}  // namespace

TEST是gtest的测试宏,我们的测试用例必须按照这样格式写,isPrimeTest是测试套的名字,一个测试套下可以有多个测试用例,那么Positive、Trivial就是我们测试用例的名称,EXPECT_EQ、EXPECT_FALSE和EXPECT_TRUE等等,都是gtest提供的测试断言,比如 EXPECT_EQ(1, Factorial(1));就是表示Factorial(1)和1是不是相等的,如果是则表示EXPECT_EQ会返回成功,否则失败,也即我们测试用例会失败或者成功。

第三步: 实现测试的main函数,当然我们也可以不用写main函数,那就需要连接gtest_main.a这个库。比如这样子编译:

g++ sample1.cc sample1_unittest.cc -lgtest -std=c++11 -lgtest_main -lpthread -o test.exe

然后运行测试程序test.exe

$ ./test

会有以下输出:  

或者是自己写一个main函数,函数定义如下:

#include <gtest/gtest.h>

int main(int argc, char** argv){
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

然后编译成我们的测试程序就OK了,也许很多同学会很惊讶为什么RUN_ALL_TESTS函数后会,我们的测试程序就能自动运行了呢? 这是一个题外话,当然我觉得这也是gtest的一大亮点。有兴趣不?



这篇关于Linux项目准备工作的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程