902|1

40

帖子

0

TA的资源

一粒金砂(中级)

楼主
 

《嵌入式软件的时间分析》在Linux环境下的C程序编译过程详解【阅读笔记1】 [复制链接]

本帖最后由 luyism 于 2024-6-25 17:32 编辑

《嵌入式软件的时间分析》在Linux环境下的C程序编译过程详解【阅读笔记1】

一、实时系统与编译过程理解

实时系统对于嵌入式工程师来说是一个基础而重要的概念。在汽车电子控制单元(ECU)等应用中,时间的精确控制和响应至关重要。而编译过程,则是将高级语言(如C语言)代码转换为可执行文件的关键步骤,其效率直接影响到系统的性能和响应速度。

二、GCC 基本使用方法

作为一名嵌入式工程师,熟练掌握 GCC 的使用对于我们的日常开发至关重要。下面,我将详细介绍 GCC 的基本用法以及编译过程的各个阶段。

GCC 的命令语法如下:

  1. gcc [选项] 输入文件名

常用选项:

  • -o:小写字母“o”,指定生成的可执行文件的名字。如果不指定,生成的可执行文件名默认为 a.out
  • -E:只进行预处理,不进行编译和汇编。
  • -S:只进行编译,不进行汇编。
  • -c:编译并汇编,但不进行链接。
  • -g:生成带有调试信息的可执行文件,方便使用 gdb 进行调试。
  • -Ox:大写字母“O”加数字,设置程序的优化等级,如 -O0-O1-O2-O3,数字越大,代码的优化等级越高,编译出来的程序一般会更小、更快,但可能会导致程序行为的变化。

除了 -g-Ox 选项,其它选项实际上是编译的分步骤,即只进行某些编译过程。

三、编译过程详解与示例

我们通过一个简单的C程序来演示在Linux环境下的完整编译过程。示例代码如下:

  1. // example.c
  2. #include <stdio.h>
  3. #define MAX_NUM 10
  4. int main() {
  5. int i;
  6. for (i = 0; i < MAX_NUM; i++) {
  7. printf("Count: %d\n", i);
  8. }
  9. return 0;
  10. }

一些相关命令如下:

  1. mkdir gcccode
  2. cd gcccode/
  3. # 使用vim直接建立源码文件,输入上述代码后保存并退出
  4. vim example.c

3.0直接编译成可执行文件

  1. gcc example.c -o example

上述命令将 example.c 直接编译成可执行文件 example

在当前目录下直接运行输出的 example 可执行文件即可看到运行效果:

  1. ./example
  2. Count: 0
  3. Count: 1
  4. Count: 2
  5. Count: 3
  6. Count: 4
  7. Count: 5
  8. Count: 6
  9. Count: 7
  10. Count: 8
  11. Count: 9

3.1 预处理(Preprocessing)

预处理阶段主要负责处理以 # 开头的预编译指令,如宏定义、头文件包含等。我们使用 gcc-E 选项来进行预处理。

命令行执行预处理:

  1. gcc -E example.c -o example.i

执行后,我们可以查看生成的 example.i 文件。预处理后的文件展示了所有头文件内容展开、宏定义替换的结果,是纯C代码形式,没有经过语法检查。直接用编辑器打开生成的hello.i,部分内容如下:

  1. 1 # 1 "example.c"
  2. 2 # 1 "<built-in>"
  3. 3 # 1 "<command-line>"
  4. 4 # 31 "<command-line>"
  5. 5 # 1 "/usr/include/stdc-predef.h" 1 3 4
  6. 6 # 32 "<command-line>" 2
  7. 7 # 1 "example.c"
  8. 8
  9. 9 # 1 "/usr/include/stdio.h" 1 3 4
  10. 10 # 27 "/usr/include/stdio.h" 3 4
  11. 11 # 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
  12. 12 # 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4
  13. 13 # 1 "/usr/include/features.h" 1 3 4
  14. 14 # 424 "/usr/include/features.h" 3 4
  15. 15 # 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
  16. 16 # 427 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
  17. 17 # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
  18. 18 # 428 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
  19. 19 # 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4
  20. 20 # 429 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
  21. 21 # 425 "/usr/include/features.h" 2 3 4
  22. 22 # 448 "/usr/include/features.h" 3 4
  23. 23 # 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
  24. 24 # 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
  25. 25 # 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
  26. 26 # 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
  27. 27 # 449 "/usr/include/features.h" 2 3 4
  28. 28 # 34 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 2 3 4
  29. 29 # 28 "/usr/include/stdio.h" 2 3 4
  30. ...
  31. (中间省略)
  32. ...
  33. 781 extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
  34. 782 # 840 "/usr/include/stdio.h" 3 4
  35. 783 extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
  36. 784
  37. 785
  38. 786
  39. 787 extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
  40. 788
  41. 789
  42. 790 extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
  43. 791 # 868 "/usr/include/stdio.h" 3 4
  44. 792
  45. 793 # 3 "example.c" 2
  46. 794
  47. 795
  48. 796
  49. 797
  50. 798 # 6 "example.c"
  51. 799 int main() {
  52. 800 int i;
  53. 801 for (i = 0; i < 10; i++) {
  54. 802 printf("Count: %d\n", i);
  55. 803 }
  56. 804 return 0;
  57. 805 }

可以看到我们的代码从12行增长到了805行,其中主要原因就是预处理阶段将 #include <stdio.h> 展开了。

3.2 编译(Compilation)

编译阶段将预处理后的代码翻译成汇编语言。我们使用 gcc-S 选项来进行编译。在这个过程,GCC会检查各个源文件的语法,即使我们调用了一个没有定义的函数,也不会报错。

命令行执行编译:

  1. gcc -S example.i -o example.s

汇编代码展示了与特定架构相关的低级指令,是CPU可以直接理解的形式,但还不是机器码。生成的 example.s 文件包含了汇编代码。生成的hello.s文件可直接使用编辑器打开,内容如下:

  1. .text
  2. .section .rodata
  3. .LC0:
  4. .string "Count: %d\n"
  5. .text
  6. .globl main
  7. .type main, @function
  8. main:
  9. .LFB0:
  10. .cfi_startproc
  11. pushq %rbp
  12. .cfi_def_cfa_offset 16
  13. .cfi_offset 6, -16
  14. movq %rsp, %rbp
  15. .cfi_def_cfa_register 6
  16. subq $16, %rsp
  17. movl $0, -4(%rbp)
  18. jmp .L2
  19. .L3:
  20. movl -4(%rbp), %eax
  21. movl %eax, %esi
  22. leaq .LC0(%rip), %rdi
  23. movl $0, %eax
  24. call printf@PLT
  25. addl $1, -4(%rbp)
  26. .L2:
  27. cmpl $9, -4(%rbp)
  28. jle .L3
  29. movl $0, %eax
  30. leave
  31. .cfi_def_cfa 7, 8
  32. ret
  33. .cfi_endproc
  34. .LFE0:
  35. .size main, .-main
  36. .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
  37. .section .note.GNU-stack,"",@progbits

在这段汇编代码中,我们可以看到以下几个关键部分:

  1. 文本段和只读数据段

    • .text.section .rodata 指示了代码段和只读数据段的开始。
    • .LC0 定义了一个字符串常量 "Count: %d\n",用于 printf 函数。
  2. 主函数 main 的定义

    • .globl main 声明 main 函数为全局符号,使其在链接时可见。
    • .type main, @function 指定 main 为函数类型。
  3. 函数体

    • main:main 函数的入口。
    • .cfi_startproc.cfi_endproc 是用于调试信息的指令。
    • pushq %rbpmovq %rsp, %rbp 用于设置栈帧。
    • subq $16, %rsp 为局部变量分配空间。
    • movl $0, -4(%rbp) 初始化局部变量。
    • jmp .L2 跳转指令,用于实现循环逻辑。
  4. 循环逻辑

    • 标签 .L3.L2 用于实现循环。
    • movl -4(%rbp), %eax 加载局部变量。
    • movl %eax, %esi 将局部变量传递给 printf
    • leaq .LC0(%rip), %rdi 加载字符串常量的地址。
    • call printf@PLT 调用 printf 函数。
    • addl $1, -4(%rbp) 增加局部变量的值。
    • cmpl $9, -4(%rbp) 比较局部变量与9。
    • jle .L3 条件跳转,若局部变量小于等于9,则继续循环。
  5. 函数结束

    • movl $0, %eax 设置返回值为0。
    • leaveret 指令用于函数返回。
  6. 元数据

    • .ident 包含编译器版本信息。
    • .section .note.GNU-stack 是GNU特定的节信息,表示该代码不需要可执行堆栈。

      3.3 汇编(Assembly)

汇编阶段将汇编代码转换成二进制目标代码(Object Code)。我们使用 gcc-c 选项来进行汇编。

命令行执行汇编:

  1. gcc -c example.s -o example.o

example.o 文件包含了机器代码的二进制表示。目标文件是二进制格式,包含了程序的机器码和必要的数据段,但尚未解决外部符号引用。

我们可以使用 objdump 工具来反汇编目标文件查看其内容。

命令行查看目标文件:

  1. objdump -d example.o

输出内容如下:

  1. example.o: 文件格式 elf64-x86-64
  2. Disassembly of section .text:
  3. 0000000000000000 <main>:
  4. 0: 55 push %rbp
  5. 1: 48 89 e5 mov %rsp,%rbp
  6. 4: 48 83 ec 10 sub $0x10,%rsp
  7. 8: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
  8. f: eb 1a jmp 2b <main+0x2b>
  9. 11: 8b 45 fc mov -0x4(%rbp),%eax
  10. 14: 89 c6 mov %eax,%esi
  11. 16: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1d <main+0x1d>
  12. 1d: b8 00 00 00 00 mov $0x0,%eax
  13. 22: e8 00 00 00 00 callq 27 <main+0x27>
  14. 27: 83 45 fc 01 addl $0x1,-0x4(%rbp)
  15. 2b: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
  16. 2f: 7e e0 jle 11 <main+0x11>
  17. 31: b8 00 00 00 00 mov $0x0,%eax
  18. 36: c9 leaveq
  19. 37: c3 retq
  1. 函数入口和栈帧设置

    1. 0000000000000000 <main>:
    2. 0: 55 push %rbp
    3. 1: 48 89 e5 mov %rsp,%rbp
    4. 4: 48 83 ec 10 sub $0x10,%rsp
    • push %rbp:将当前的基址指针压入栈中,保存调用者的栈帧指针。
    • mov %rsp,%rbp:将栈指针的值赋给基址指针,建立新的栈帧。
    • sub $0x10,%rsp:为局部变量分配16字节的空间。
  2. 初始化和循环跳转

    1. 8: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
    2. f: eb 1a jmp 2b <main+0x2b>
    • movl $0x0,-0x4(%rbp):将局部变量初始化为0。
    • jmp 2b <main+0x2b>:跳转到循环的条件检查部分。
  3. 循环体

    1. 11: 8b 45 fc mov -0x4(%rbp),%eax
    2. 14: 89 c6 mov %eax,%esi
    3. 16: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1d <main+0x1d>
    4. 1d: b8 00 00 00 00 mov $0x0,%eax
    5. 22: e8 00 00 00 00 callq 27 <main+0x27>
    6. 27: 83 45 fc 01 addl $0x1,-0x4(%rbp)
    • mov -0x4(%rbp),%eax:将局部变量的值加载到 eax 寄存器。
    • mov %eax,%esi:将 eax 寄存器的值传递给 esi 寄存器(作为 printf 参数)。
    • lea 0x0(%rip),%rdi:加载字符串常量地址到 rdi 寄存器(作为 printf 参数)。
    • mov $0x0,%eax:将 eax 寄存器置零。
    • callq 27 <main+0x27>:调用 printf 函数。
    • addl $0x1,-0x4(%rbp):将局部变量增加1。
  4. 循环条件检查和函数结束

    1. 2b: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
    2. 2f: 7e e0 jle 11 <main+0x11>
    3. 31: b8 00 00 00 00 mov $0x0,%eax
    4. 36: c9 leaveq
    5. 37: c3 retq
    • cmpl $0x9,-0x4(%rbp):比较局部变量与9。
    • jle 11 <main+0x11>:如果小于等于9,则跳转回循环体。
    • mov $0x0,%eax:将 eax 寄存器置零,作为函数返回值。
    • leaveq:恢复调用者的栈帧。
    • retq:返回到调用者。

3.4 链接(Linking)

链接阶段将一个或多个目标文件与任何所需的库文件合并,解决符号引用,生成最终的可执行文件。我们使用 gcc 直接生成可执行文件。

命令行执行链接:

  1. gcc example.o -o example

此命令将 example.o 文件与标准库和其他必要的库文件链接成最终的可执行文件 example

四、优化选项

通过上述步骤,我们了解了从高级C代码到最终可执行文件的转换过程。在实际工作中,优化编译过程可以通过调整编译器参数、选择合适的优化级别以及利用特定的硬件功能来提升代码执行效率和系统响应速度。

若想优化编译过程,可以在编译时加入优化等级标志,例如:举个例子,我们可以通过 -O 选项来进行编译器优化:

-O2 表示中等优化级别,它会启用大多数优化选项,以提高运行时性能而不显著增加编译时间。

  1. gcc -o optimized_example example.c -O2

五、分析和查看Linux下的可执行文件 example

在经过编译、汇编和链接后,我们最终生成了可执行文件 example。接下来,我们将详细分析如何查看和分析这个可执行文件,并通过运行它来验证程序的功能。

我们可以使用 objdumpreadelf 工具来查看可执行文件的详细信息。

  1. 使用 objdump 查看可执行文件的反汇编代码
  1. objdump -d example

该命令会反汇编可执行文件,显示所有的机器指令,具体的代码上面已经分析。

  1. 使用 readelf 查看可执行文件的头部信息和段信息

在Linux下可执行文件是ELF (Executable and Linkable Format) 格式,我们可以使用 readelf 工具可以深入分析 ELF文件的头部信息和段信息,帮助我们理解文件的结构和加载方式。

下面是一个示例输出以及详细解释。

  1. readelf -h example

输出内容如下所示:

  1. ELF 头:
  2. Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  3. 类别: ELF64
  4. 数据: 2 补码,小端序 (little endian)
  5. 版本: 1 (current)
  6. OS/ABI: UNIX - System V
  7. ABI 版本: 0
  8. 类型: DYN (共享目标文件)
  9. 系统架构: Advanced Micro Devices X86-64
  10. 版本: 0x1
  11. 入口点地址: 0x540
  12. 程序头起点: 64 (bytes into file)
  13. Start of section headers: 6448 (bytes into file)
  14. 标志: 0x0
  15. 本头的大小: 64 (字节)
  16. 程序头大小: 56 (字节)
  17. Number of program headers: 9
  18. 节头大小: 64 (字节)
  19. 节头数量: 29
  20. 字符串表索引节头: 28

详细解释

  1. Magic:

    • 标识文件类型为 ELF 文件,前四个字节 7f 45 4c 46 分别对应于字符 .ELF
  2. 类别:

    • 指示文件是 64 位还是 32 位。这里是 64 位(ELF64)。
  3. 数据:

    • 指示数据编码格式,这里是小端序(little endian)。
  4. 版本:

    • 表示 ELF 文件的版本,目前为 1。
  5. OS/ABI:

    • 指示操作系统和应用程序二进制接口 (ABI)。这里是 UNIX 系统 V。
  6. ABI 版本:

    • 指示 ABI 的版本,这里是 0。
  7. 类型:

    • 指示文件类型,这里是 DYN,表示共享目标文件(例如动态链接库)。
  8. 系统架构:

    • 指示目标机器架构,这里是 x86-64 架构。
  9. 版本:

    • ELF 文件格式版本,通常为 0x1。
  10. 入口点地址:

    • 程序执行开始的地址,当系统加载这个可执行文件时,将控制权交给这个地址。这里是 0x540
  11. 程序头起点:

    • 程序头表在文件中的偏移量(以字节为单位),这里是 64 字节。
  12. 节头起点:

    • 节头表在文件中的偏移量(以字节为单位),这里是 6448 字节。
  13. 标志:

    • 与文件相关的特定标志。这里是 0x0,表示没有特别的标志。
  14. 本头的大小:

    • ELF 文件头的大小,通常为 64 字节。
  15. 程序头大小:

    • 每个程序头表项的大小,通常为 56 字节。
  16. 程序头数量:

    • 程序头表的条目数,这里是 9 个。
  17. 节头大小:

    • 每个节头表项的大小,通常为 64 字节。
  18. 节头数量:

    • 节头表的条目数,这里是 29 个。
  19. 字符串表索引节头:

    • 字符串表索引节头表项的索引,这里是 28。

通过 readelf -h 命令,我们可以了解 ELF 可执行文件的基本结构,包括文件类型、机器架构、入口点地址和程序头表及节头表的位置和大小等信息。这些信息对于理解可执行文件的加载和执行过程非常重要,有助于嵌入式系统开发人员进行调试和优化。

此帖出自汽车电子论坛

最新回复

 掌握好readelf -h 命令,还是对帮助嵌入式系统开发人员进行调试和优化有很大帮助的。   详情 回复 发表于 2024-6-26 07:22
点赞 关注

回复
举报

7032

帖子

0

TA的资源

五彩晶圆(高级)

沙发
 

 掌握好readelf -h 命令,还是对帮助嵌入式系统开发人员进行调试和优化有很大帮助的。

此帖出自汽车电子论坛
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
推荐帖子
ARM开发全集

ARM开发全集1~6

Protel 99从入门到精通教程(中)

接上Protel 99从入门到精通教程(上)

空中鼠标-MSP-EXP430FR5739实验板实现

玩跑跑卡丁车突发奇想,可不可以用团购来的MSP-EXP430FR5739实验板来控制赛车!基本思路是这样的,利用EXP430FR5739实验板上的三 ...

2013年国赛元器件清单重点分析!(随时更新)

>>2013年器件清单对比及分析_by chbaaic >>分析2013年器件清单中几个比较不寻常的器件_by longhaozheng >>官 ...

神器系列-艾刷做开发板

本帖最后由 dcexpert 于 2016-2-29 00:16 编辑 前几天发了一篇《神器拆机-艾刷》的帖子,发现里面使用了STM32F401CC单片机, ...

通讯系统离不开的滤波器系列2

516242516243 516244 516245516246516247516248516249516250

关于答题活动抽奖无正确答案的问题

本帖最后由 sanhuasr 于 2020-12-16 21:21 编辑 以科技之力,成就安全 PI与您一起驾驭明天的智慧 答题赢好礼! https://www.e ...

免费申请:国产FPGA 高云家小蜜蜂家族GW1N 系列开发板

开发板型号:DK_MINI_GW1N-LV4LQ144C6I5_V1.1(共3套) 来自:高云半导体 市场价:299元 在论坛>>littleshrimp ...

【得捷Follow me第4期】综合实践之智能家居控制器

本帖最后由 鲜de芒果 于 2024-2-22 16:27 编辑 1 任务说明 这次我准备使用W5500-EVB-Pico作为一个控制器, 通过 MQTT 协议 ...

记一次Luckfox幸狐 RV1106 Linux 开发板移植gt911触摸驱动过程中的踩坑排查记录

最近为Luckfox Pico pro max 开发板移植gt911触摸驱动,踩了个不大不小的坑,发现基本没人提及这块,分享出来大家参考 拉取官 ...

关闭
站长推荐上一条 1/10 下一条
有奖直播:当AI遇见仿真,会有什么样的电子行业革新之路?
首场直播:Simcenter AI 赋能电子行业研发创新
直播时间:04月15日14:00-14:50

查看 »

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

 
机器人开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网 7

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表