0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Linux使用gcc编译程序的语法

科技绿洲 来源:工程师进阶笔记 作者:工程师进阶笔记 2023-06-22 10:51 次阅读

01. 调试相关的宏

Linux使用gcc编译程序的时候,对于调试的语句还具有一些特殊的语法。

gcc编译的过程中,会生成一些宏,可以使用这些宏分别打印当前源文件的信息,主要内容是当前的文件、当前运行的函数和当前的程序行。

具体宏如下:

__FILE__  当前程序源文件 (char*)
__FUNCTION__  当前运行的函数 (char*)
__LINE__  当前的函数行 (int)

这些宏不是程序代码定义的,而是有编译器产生的。这些信息都是在编译器处理文件的时候动态产生的。

「测试示例:」

#include < stdio.h >

int main(void)
{
    printf("file: %s\\n", __FILE__);
    printf("function: %s\\n", __FUNCTION__);
    printf("line: %d\\n", __LINE__);

    return 0;
}

02. # 字符串化操作符

在gcc的编译系统中,可以使用#将当前的内容转换成字符串。

「程序示例:」

#include < stdio.h >

#define DPRINT(expr) printf("< main >%s = %d\\n", #expr, expr);

int main(void)
{
    int x = 3;
    int y = 5;

    DPRINT(x / y);
    DPRINT(x + y);
    DPRINT(x * y);
    
    return 0;
}

「执行结果:」

deng@itcast:~/tmp$ gcc test.c 
deng@itcast:~/tmp$ ./a.out  
< main >x / y = 0
< main >x + y = 8
< main >x * y = 15

#expr表示根据宏中的参数(即表达式的内容),生成一个字符串。该过程同样是有编译器产生的,编译器在编译源文件的时候,如果遇到了类似的宏,会自动根据程序中表达式的内容,生成一个字符串的宏。

这种方式的优点是可以用统一的方法打印表达式的内容,在程序的调试过程中可以方便直观的看到转换字符串之后的表达式。

具体的表达式的内容是什么,有编译器自动写入程序中,这样使用相同的宏打印所有表达式的字符串。

//打印字符
#define debugc(expr) printf("< char > %s = %c\\n", #expr, expr)
//打印浮点数
#define debugf(expr) printf("< float > %s = %f\\n", #expr, expr)
//按照16进制打印整数
#define debugx(expr) printf("< int > %s = 0X%x\\n", #expr, expr);

由于#expr本质上市一个表示字符串的宏,因此在程序中也可以不适用%s打印它的内容,而是可以将其直接与其它的字符串连接。

因此,上述宏可以等价以下形式:

//打印字符
#define debugc(expr) printf("< char > #expr = %c\\n", expr)
//打印浮点数
#define debugf(expr) printf("< float > #expr = %f\\n", expr)
//按照16进制打印整数
#define debugx(expr) printf("< int > #expr = 0X%x\\n", expr);

「总结:」

#是C语言预处理阶段的字符串化操作符,可将宏中的内容转换成字符串。

03. ## 连接操作符

在gcc的编译系统中,##是C语言中的连接操作符,可以在编译的预处理阶段实现字符串连接的操作。

「程序示例:」

#include < stdio.h >

#define test(x) test##x

void test1(int a)
{
    printf("test1 a = %d\\n", a);
}

void test2(char *s)
{
    printf("test2 s = %s\\n", s);
}

int main(void)
{
    test(1)(100);

    test(2)("hello world");
    
    return 0;
}

上述程序中,test(x)宏被定义为test##x, 他表示test字符串和x字符串的连接。

在程序的调试语句中,##常用的方式如下

#define DEBUG(fmt, args...) printf(fmt, ##args)

替换的方式是将参数的两个部分以##连接。##表示连接变量代表前面的参数列表。使用这种形式可以将宏的参数传递给一个参数。args…是宏的参数,表示可变的参数列表,使用##args将其传给printf函数.

「总结:」

##是C语言预处理阶段的连接操作符,可实现宏参数的连接。

04. 调试宏第一种形式

一种定义的方式:

#define DEBUG(fmt, args...)             \\
    {                                   \\
    printf("file:%s function: %s line: %d ", __FILE__, __FUNCTION__, __LINE__);\\
    printf(fmt, ##args);                \\
    }

「程序示例:」

#include < stdio.h >

#define DEBUG(fmt, args...)             \\
    {                                   \\
    printf("file:%s function: %s line: %d ", __FILE__, __FUNCTION__, __LINE__);\\
    printf(fmt, ##args);                \\
    }


int main(void)
{
    int a = 100;
    int b = 200;

    char *s = "hello world";
    DEBUG("a = %d b = %d\\n", a, b);
    DEBUG("a = %x b = %x\\n", a, b);
    DEBUG("s = %s\\n", s);
    
    return 0;
}

「总结:」

上面的DEBUG定义的方式是两条语句的组合,不可能在产生返回值,因此不能使用它的返回值。

05. 调试宏的第二种定义方式

调试宏的第二种定义方式

#define DEBUG(fmt, args...)             \\
    printf("file:%s function: %s line: %d "fmt, \\
    __FILE__, __FUNCTION__, __LINE__, ##args)

程序示例

#include < stdio.h >

#define DEBUG(fmt, args...)             \\
    printf("file:%s function: %s line: %d "fmt, \\
    __FILE__, __FUNCTION__, __LINE__, ##args)


int main(void)
{
    int a = 100;
    int b = 200;

    char *s = "hello world";
    DEBUG("a = %d b = %d\\n", a, b);
    DEBUG("a = %x b = %x\\n", a, b);
    DEBUG("s = %s\\n", s);
    
    return 0;
}

「总结:」

fmt必须是一个字符串,不能使用指针,只有这样才可以实现字符串的功能。

06. 对调试语句进行分级审查

即使定义了调试的宏,在工程足够大的情况下,也会导致在打开宏开关的时候在终端出现大量的信息。而无法区分哪些是有用的。

这个时候就要加入分级检查机制,可以定义不同的调试级别,这样就可以对不同重要程序和不同的模块进行区分,需要调试哪一个模块就可以打开那一个模块的调试级别。

一般可以利用配置文件的方式显示,其实Linux内核也是这么做的,它把调试的等级分成了7个不同重要程度的级别,只有设定某个级别可以显示,对应的调试信息才会打印到终端上。

可以写出一下配置文件

[debug]
debug_level=XXX_MODULE

解析配置文件使用标准的字符串操作库函数就可以获取XXX_MODULE这个数值。

int show_debug(int level)
{
    if (level == XXX_MODULE)
    {
        #define DEBUG(fmt, args...)             \\
        printf("file:%s function: %s line: %d "fmt, \\
        __FILE__, __FUNCTION__, __LINE__, ##args)       
    }
    else if (...)
    {
        ....
    }
}

07. 条件编译调试语句

在实际的开发中,一般会维护两种源程序,一种是带有调试语句的调试版本程序,另外一种是不带有调试语句的发布版本程序。

然后根据不同的条件编译选项,编译出不同的调试版本和发布版本的程序。

在实现过程中,可以使用一个调试宏来控制调试语句的开关。

#ifdef USE_DEBUG
        #define DEBUG(fmt, args...)             \\
        printf("file:%s function: %s line: %d "fmt, \\
        __FILE__, __FUNCTION__, __LINE__, ##args)  
#else
  #define DEBUG(fmt, args...)

#endif

如果USE_DEBUG被定义,那么有调试信息,否则DEBUG就为空。

如果需要调试信息,就只需要在程序中更改一行就可以了。

#define USE_DEBUG
#undef USE_DEBUG

定义条件编译的方式使用一个带有值的宏

#if USE_DEBUG
        #define DEBUG(fmt, args...)             \\
        printf("file:%s function: %s line: %d "fmt, \\
        __FILE__, __FUNCTION__, __LINE__, ##args)  
#else
  #define DEBUG(fmt, args...)

#endif

可以使用如下方式进行条件编译

#ifndef USE_DEBUG
#define USE_DEBUG 0
#endif

08. 使用do…while的宏定义

使用宏定义可以将一些较为短小的功能封装,方便使用。宏的形式和函数类似,但是可以节省函数跳转的开销。

如何将一个语句封装成一个宏,在程序中常常使用do…while(0)的形式。

#define HELLO(str) do { \\
printf("hello: %s\\n", str); \\
}while(0)

「程序示例:」

int cond = 1;
if (cond)
    HELLO("true");
else
    HELLO("false");

09. 代码剖析

对于比较大的程序,可以借助一些工具来首先把需要优化的点清理出来。接下来我们来看看在程序执行过程中获取数据并进行分析的工具:代码剖析程序。

「测试程序:」

#include < stdio.h >


#define T 100000

void call_one()
{
    int count = T * 1000;
    while(count--);
}

void call_two()
{
    int count = T * 50;
    while(count--);
}

void call_three()
{
    int count = T * 20;
    while(count--);
}


int main(void)
{
    int time = 10;

    while(time--)
    {
        call_one();
        call_two();
        call_three();
    }
    
    return 0;
}

编译的时候加入-pg选项:

deng@itcast:~/tmp$ gcc -pg  test.c -o test

执行完成后,在当前文件中生成了一个gmon.out文件。

deng@itcast:~/tmp$ ./test  
deng@itcast:~/tmp$ ls
gmon.out  test  test.c
deng@itcast:~/tmp$

「使用gprof剖析主程序:」

deng@itcast:~/tmp$ gprof test
Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 95.64      1.61     1.61       10   160.68   160.68  call_one
  3.63      1.67     0.06       10     6.10     6.10  call_two
  2.42      1.71     0.04       10     4.07     4.07  call_three

其中主要的信息有两个,一个是每个函数执行的时间占程序总时间的百分比,另外一个就是函数被调用的次数。通过这些信息,可以优化核心程序的实现方式来提高效率。

当然这个剖析程序由于它自身特性有一些限制,比较适用于运行时间比较长的程序,因为统计的时间是基于间隔计数这种机制,所以还需要考虑函数执行的相对时间,如果程序执行时间过短,那得到的信息是没有任何参考意义的。

「将上诉程序时间缩短:」

#include < stdio.h >


#define T 100

void call_one()
{
    int count = T * 1000;
    while(count--);
}

void call_two()
{
    int count = T * 50;
    while(count--);
}

void call_three()
{
    int count = T * 20;
    while(count--);
}


int main(void)
{
    int time = 10;

    while(time--)
    {
        call_one();
        call_two();
        call_three();
    }
    
    return 0;
}

「剖析结果如下:」

deng@itcast:~/tmp$ gcc -pg test.c -o test
deng@itcast:~/tmp$ ./test  
deng@itcast:~/tmp$ gprof test
Flat profile:

Each sample counts as 0.01 seconds.
 no time accumulated

  %   cumulative   self              self     total           
 time   seconds   seconds    calls  Ts/call  Ts/call  name    
  0.00      0.00     0.00       10     0.00     0.00  call_one
  0.00      0.00     0.00       10     0.00     0.00  call_three
  0.00      0.00     0.00       10     0.00     0.00  call_two

因此该剖析程序对于越复杂、执行时间越长的函数也适用。

那么是不是每个函数执行的绝对时间越长,剖析显示的时间就真的越长呢?可以再看如下的例子

#include < stdio.h >


#define T 100

void call_one()
{
    int count = T * 1000;
    while(count--);
}

void call_two()
{
    int count = T * 100000;
    while(count--);
}

void call_three()
{
    int count = T * 20;
    while(count--);
}


int main(void)
{
    int time = 10;

    while(time--)
    {
        call_one();
        call_two();
        call_three();
    }
    
    return 0;
}

「剖析结果如下:」

deng@itcast:~/tmp$ gcc -pg test.c -o test
deng@itcast:~/tmp$ ./test  
deng@itcast:~/tmp$ gprof test
Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
101.69      0.15     0.15       10    15.25    15.25  call_two
  0.00      0.15     0.00       10     0.00     0.00  call_one
  0.00      0.15     0.00       10     0.00     0.00  call_three

「总结:」

在使用gprof工具的时候,对于一个函数进行gprof方式的剖析,实质上的时间是指除去库函数调用和系统调用之外,纯碎应用部分开发的实际代码运行的时间,也就是说time一项描述的时间值不包括库函数printf、系统调用system等运行的时间。

这些实用库函数的程序虽然运行的时候将比最初的程序实用更多的时间,但是对于剖析函数来说并没有影响。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    87

    文章

    11302

    浏览量

    209426
  • 函数
    +关注

    关注

    3

    文章

    4329

    浏览量

    62588
  • 编译程序
    +关注

    关注

    0

    文章

    12

    浏览量

    4134
收藏 人收藏

    评论

    相关推荐

    LabVIEW编译程序设计

      编译程序设计是一个复杂的话题,即使对内行的软件工程师来说也要考虑很多专业知识。   NI LabVIEW软件是一种多规
    发表于 10-16 11:03 2618次阅读
    LabVIEW<b class='flag-5'>编译程序</b>设计

    Linux编程】如何使用gcc编译源代码时输出map文件?

    Linux编程】如何使用gcc编译源代码时输出map文件?
    的头像 发表于 08-15 14:08 8252次阅读
    【<b class='flag-5'>Linux</b>编程】如何使用<b class='flag-5'>gcc</b><b class='flag-5'>编译</b>源代码时输出map文件?

    嵌入式软件项目中的一些常用套路与技巧

    Linux使用gcc编译程序的时候,对于调试的语句还具有一些特殊的语法
    发表于 05-23 11:04 382次阅读

    LinuxGCC编译

    一、Linux 下多文件编译 在上一篇 Linux 下的 C 编程我们知道了 Linux 下的编译器为
    的头像 发表于 09-11 15:18 2624次阅读
    <b class='flag-5'>Linux</b> 下<b class='flag-5'>GCC</b>的<b class='flag-5'>编译</b>

    使用GCC编译程序编译过程

    一.使用GCC编译程序时,编译过程可以细分为四个阶段:预处理(Pre-Processing)编译(Compiling)汇编(Assembling)链接(Linking)二.
    发表于 12-15 08:30

    为什么arm-linux-gcc无法编译程序

    为什么arm-linux-gcc无法编译程序呢?是什么原因造成的?求大神指导
    发表于 07-13 11:55

    鸿蒙系统移植:编译第一个APP:hello

    不一样): arm-linux-gcc  -o  hello   hello.c 在Liteos-a中,使用LLVM来编译程序。LLVM的本意是“Low Level Virtual Machine”,一个底层的虚拟机。
    发表于 10-20 16:18 17次下载
    鸿蒙系统移植:<b class='flag-5'>编译</b>第一个APP:hello

    关于嵌入式Linux调试相关的宏

    Linux使用gcc编译程序的时候,对于调试的语句还具有一些特殊的语法gcc编译的过程中,会
    的头像 发表于 07-06 16:29 952次阅读

    Linux编程】如何使用gcc生成预编译文件?

    Linux编程】如何使用gcc生成预编译文件?
    的头像 发表于 08-31 13:07 1953次阅读
    【<b class='flag-5'>Linux</b>编程】如何使用<b class='flag-5'>gcc</b>生成预<b class='flag-5'>编译</b>文件?

    RX族C/C++编译程序、汇编程序、优化连接编辑程序 编译程序包 用户手册 Rev.1.00

    RX族C/C++编译程序、汇编程序、优化连接编辑程序 编译程序包 用户手册 Rev.1.00
    发表于 04-17 19:24 0次下载
    RX族C/C++<b class='flag-5'>编译程序</b>、汇编<b class='flag-5'>程序</b>、优化连接编辑<b class='flag-5'>程序</b> <b class='flag-5'>编译程序</b>包 用户手册 Rev.1.00

    SuperHTM RISC引擎C/C++编译程序、汇总程序、优化连接编译程序 编译程序包V.9.01 用户手册

    SuperHTM RISC引擎C/C++编译程序、汇总程序、优化连接编译程序 编译程序包V.9.01 用户手册
    发表于 05-04 18:40 0次下载
    SuperHTM RISC引擎C/C++<b class='flag-5'>编译程序</b>、汇总<b class='flag-5'>程序</b>、优化连接<b class='flag-5'>编译程序</b> <b class='flag-5'>编译程序</b>包V.9.01 用户手册

    H8S、H8/300系列C/C++编译程序、汇总程序、优化连接编译程序(RCJ10B0001-0100)

    H8S、H8/300系列C/C++编译程序、汇总程序、优化连接编译程序(RCJ10B0001-0100)
    发表于 05-09 20:00 0次下载
    H8S、H8/300系列C/C++<b class='flag-5'>编译程序</b>、汇总<b class='flag-5'>程序</b>、优化连接<b class='flag-5'>编译程序</b>(RCJ10B0001-0100)

    Linux C开发中的一些常用的调试技巧

    Linux使用gcc编译程序的时候,对于调试的语句还具有一些特殊的语法
    发表于 06-01 09:03 247次阅读

    H8S、H8/300系列C/C++编译程序、汇总程序、优化连接编译程序(RCJ10B0001-0100)

    H8S、H8/300系列C/C++编译程序、汇总程序、优化连接编译程序(RCJ10B0001-0100)
    发表于 06-28 18:50 0次下载
    H8S、H8/300系列C/C++<b class='flag-5'>编译程序</b>、汇总<b class='flag-5'>程序</b>、优化连接<b class='flag-5'>编译程序</b>(RCJ10B0001-0100)

    嵌入式Linux C语言编程程序调试与宏定义

    Linux使用gcc编译程序的时候,对于调试的语句还具有一些特殊的语法gcc编译的过程中,会
    发表于 03-01 11:41 1007次阅读