新沂网站优化百度地图推广怎么收费标准
web/
2025/9/26 21:23:21/
文章来源:
新沂网站优化,百度地图推广怎么收费标准,虾米wordpress插件,网站开发前期准备工作《嵌入式工程师自我修养/C语言》系列——程序的编译、链接过程分析#xff08;简洁浓缩版#xff09;#xff01; 一、程序的编译1.1 预编译指令 pragma1.2 编译过程概述1.3 符号表和重定位表 二、程序的链接2.1 分段组装2.2 符号决议2.2.1 强符号与弱符号2.2.2 GNU编译器的… 《嵌入式工程师自我修养/C语言》系列——程序的编译、链接过程分析简洁浓缩版 一、程序的编译1.1 预编译指令 pragma1.2 编译过程概述1.3 符号表和重定位表 二、程序的链接2.1 分段组装2.2 符号决议2.2.1 强符号与弱符号2.2.2 GNU编译器的__attribute__((weak))属性 2.3 重定位2.3.1 重定位的基本原理2.3.2 重定位操作的流程 三、结语 快速学习嵌入式开发其他基础知识 返回专栏总目录 《嵌入式工程师自我修养/C语言》 一、程序的编译 众所周知程序的编译过程包括预处理、编译、汇编、链接预处理过程中的很多预处理指令都很简单这里就不赘述了诸如#if #else #endif这种预处理指令这里仅先介绍一种大家可能不太熟悉的预处理指令#pragma。
1.1 预编译指令 pragma 尤其在C和C编程中预编译指令#pragma充当着一个强大的工具它允许我们向编译器传达特殊的指令来控制编译过程的各个方面。我们常用的几种用法列举如下
#pragma pack([n]) 在结构体数据排布中#pragma pack([n])是一个常用的指令用于指定结构体或联合体成员的对齐方式。默认情况下编译器会按照特定的规则通常是结构体最大成员的大小对成员进行对齐这可能会导致内存的浪费。通过使用#pragma pack我们可以减少这种浪费优化内存的使用。
#pragma pack(push, 1) // 保存当前对齐设置新的对齐为1字节
struct MyStruct {char a; // 1 byteint b; // 4 byteschar c; // 1 byte
};
#pragma pack(pop) // 恢复之前的对齐设置上面的代码段将结构体MyStruct的对齐设置为1字节这意味着不会有填充字节用于对齐从而减小结构体的总大小。
#pragma message(“string”) 在复杂的项目开发过程中#pragma message用于在编译时输出自定义的信息或提醒它可以作为一种在代码中留下注释或提醒的手段。
#pragma message(Compiling the module...)
void myFunction() {// ...
}当编译包含这段代码的文件时编译器会显示消息Compiling the module…这能够帮助开发者注意到特定的代码块或编译阶段。
#pragma warning 对于想要控制特定警告的开发者来说#pragma warning是一个非常有用的指令。它可以允许你禁用或启用特定的警告有助于保持代码的清洁和警告的相关性。
#pragma warning(disable : 4996) // 禁用4996警告
strcpy(dest, src);
#pragma warning(default : 4996) // 恢复4996警告的默认行为这里我们禁用了与编号为4996的警告相关的编译器警告然后在需要的地方恢复了它。
#pragma once 在大型项目中防止头文件被多次包含是至关重要的#pragma once是一个非常高效的预编译指令来实现这一点。它告诉编译器只包含一次头文件避免了传统宏定义的冗余和潜在的命名冲突。
#pragma once
// 声明和定义...在这个例子中只要包含这个头文件编译器就会确保它在编译单元中只被包含一次。 #pragma是开发者手中的一把瑞士军刀无论是进行内存管理优化、代码诊断、抑制编译器警告还是头文件的管理它都能发挥巨大的作用。尽管使用#pragma需要对它们的影响有深入的了解但合理利用它们可以在许多方面提高代码的质量和编译的效率。
Tip每个编译器对#pragma的支持可能略有不同因此在跨编译器项目中使用时请务必检查兼容性。
1.2 编译过程概述 从C程序到可执行文件整个编译过程如下所示编译程序如gcc、arm-linux-gcc会调用不同的工具来完成不同阶段的任务。
预处理器将源文件main.c经过预处理变为main.i编译器将预处理后的main.i编译为汇编文件main.s汇编器将汇编文件main.s编译为目标文件main.o链接器将各个目标文件main.o、sub.o链接成可执行文件a.out。 最后生成的可执行文件a.out 其实也是目标文件object file唯一不同的是a.out是一种可执行的目标文件。目标文件一般可以分为3种可重定位的目标文件relocatable files、可执行的目标文件executable files和可被共享的目标文件shared object files。
Tip汇编器生成的目标文件是可重定位的目标文件是不可执行的需要链接器经过链接、重定位之后才能运行。可被共享的目标文件一般以共享库的形式存在在程序运行时需要动态加载到内存跟应用程序一起运行。 从C源文件到汇编文件的转换其实就是将C文件中的程序代码块、函数转换为汇编程序中的代码段将C程序中的全局变量、静态变量、常量转换为汇编程序中的数据段、只读数据段。这已经是及其简洁的说法了实际上编译过程可以分为以下6步每一步都是可以深入探讨的但我们没必要了解的那么深入简单知道其作用即可他们分别是词法分析、语法分析、语义分析、中间代码生成、汇编代码生成、目标代码生成。 汇编文件中的汇编指令就是二进制指令的助记符唯一的差异就是汇编语言的程序结构需要使用各种伪操作来组织。汇编文件经过汇编器汇编后处理掉各种伪操作命令就是二进制目标文件了。每一个c源文件经过汇编都会生成对应的目标文件这些目标文件是不可执行的属于可重定位的目标文件它们要经过链接器重定位、链接之后才能组装成一个可执行的目标文件。这些目标文件都是以零地址为链接起始地址进行链接的比如上图中main.c 和 sub.c经过汇编生成的可执行文件main.o 和 sub.o如果通过readelf -S main.o这个-S选项分别查看他们节头表的话将发现两个文件的起始段信息都是从0地址开始往后偏移。在每个可重定位目标文件中函数或变量的地址其实就是它们在文件中相对于零地址的偏移。
1.3 符号表和重定位表 在编译和链接程序的过程中编译器将源代码编译成目标文件每个目标文件都是假设自己从零地址开始。在链接过程中链接器需要把多个目标文件合并成一个可执行文件这时就需要更新文件中的地址引用这个更新地址的过程称为重定位。 链接器如何知道哪些地址需要更新呢答案是通过重定位表这是一种记录了需要更新地址的符号信息的数据结构。每个目标文件都包含有自己的重定位表在链接时链接器会查看这些表然后根据最终确定的内存布局来更新符号地址。(可以使用readelf -sr main.o查看目标文件的重定位表) 同时每个目标文件还包含一个符号表这个表列出了文件中所有的符号如函数和变量名无论这些符号是否需要重定位。链接器通过符号表来解析不同目标文件之间的符号引用关系。(可以使用readelf -s main.o查看目标文件的符号表) 例如如果main.o文件中的main函数调用了sub.o文件中的add和sub函数在链接过程中add和sub函数的地址可能会改变。链接器会在这个过程结束后根据重定位表中的信息更新这些函数引用的地址。这就确保了在最终生成的可执行文件中所有的函数和变量引用都指向正确的地址。
Tip符号表本质上是一个结构体数组在ARM平台下定义在Linux内核源码的/arch/arm/include/asm/elf.h文件中。 在编译和链接程序的时候符号表中的每个符号都有一个符号值和类型。符号值是符号在内存中的地址类型描述了符号的性质和用途。这些类型包括
符号类型描述OBJECT表示变量FUNC表示函数或可执行代码FILE关联当前目标文件名称SECTION与一个section有关用于重定位COMMON表示公用块数据对象是全局弱符号TLS变量存储在线程局部存储NOTYPE符号类型未指定或未知
Tip符号值既可以是绝对地址通常在可执行目标文件中出现也可以是相对地址常在可重定位目标文件中出现。
二、程序的链接 通过编译环节我们得到了各个文件对应的目标文件每个目标文件都是由section构成的可重定位目标文件其中还有两个重要的表重定位表和符号表。本节将简述链接器具体如何应用这两个表。实际上链接过程主要分三步分段组装、符号决议和重定位。
2.1 分段组装 分段组装过程其实就是链接器将各个目标文件的各类型的段组装在一起比如将各个目标文件的代码段放在一起数据段放在一起建立一个全局符号表收集各个目标文件中的符号信息注意此时全局符号表中各个符号的地址仍然是原来在各个目标文件中的地址即都是以零地址作为起始地址的 在链接程序时需要指定一个链接起始地址因为链接生成的可执行文件最终是要被加载到内存中执行的该地址一般也是程序要加载到内存中的地址。同时还要考虑链接过程中各个section的排布顺序等方面这些功能都是通过连接脚本实现的。 这个脚本文件里不仅规定了各个段的组装顺序、起始地址、位置对齐等信息同时对输出的可执行文件格式、运行平台、入口地址等信息做了详细的描述。链接器就是根据链接脚本定义的规则来组装可执行文件的并最终将这些信息以section的形式保存到可执行文件的ELF Header中。 一个简单的链接脚本示例如下所示关于链接脚本的详细内容将在另一篇文章中阐述届时将在这里放置文章链接。
/* Specify the output format and the target architecture */
OUTPUT_FORMAT(elf64-x86-64)
OUTPUT_ARCH(i386:x86-64)ENTRY(_start) /* Define the entry point of the executable *//* Define the memory layout of the sections */
SECTIONS
{. 0x10000; /* Define the starting memory address for the code */.text : {*(.text) /* Include all .text sections from input files */}.data : {*(.data) /* Include all .data sections from input files */}.bss : {*(.bss) /* Include all .bss sections from input files */}
}在这个链接脚本中我们定义了输出格式为elf64-x86-64目标架构为i386:x86-64。脚本中ENTRY指令指定了程序的入口点为_start。在SECTIONS块中我们为.text、.data和.bss三个部分指定了它们在内存中的起始地址和排布顺序。
Tips 1. 不同的编译器、不同的操作系统链接脚本的文件名后缀一般也不一样。 2. 在一个由带有MMU的CPU搭建的嵌入式系统中程序的链接起始地址往往都是一个虚拟地址程序运行过程中还需要地址转换通过MMU将虚拟地址转换为物理地址然后才能访问内存。
2.2 符号决议 在软件开发的链接阶段程序中的各个符号如变量和函数名需要被合并以形成最终的可执行文件。符号冲突通常是指不同的对象或源文件中含有相同名称的全局符号。解决这种冲突的规则可以用一句俗语概括“一山不容二虎强弱可以共存体积大者胜出”。为了更好理解这些概念我们首先需要区分“强符号”与“弱符号”。
2.2.1 强符号与弱符号 强符号通常是指用户定义的全局变量和函数因为它们有具体的值或实现。而弱符号则可能来自于未初始化的全局变量或者是一些特殊的编译器指令如GNU编译器的__attribute__((weak))声明的变量和函数。在链接时强符号与弱符号的处理有所不同这导致了一些链接时的规则和行为。
——链接规则解释 一山不容二虎这条规则指出如果两个强符号冲突即两个强符号的名称相同则链接会失败因为链接器不允许有两个相同名称的强符号存在。这就像一山不能容纳两只虎一样不可能有两个相同的实体共存。 强弱可以共存当一个强符号和一个弱符号名称相同时强符号会胜出链接器会选择强符号而忽略弱符号。这是因为强符号提供了明确的定义而弱符号则相对模糊。 体积大者胜出如果两个符号都是弱符号并且都有相同的名称链接器会选择那个体积更大的符号即占用内存空间更多的符号并且忽略体积较小的那个。如果它们大小相同则任选其一。
——举例如下 假设我们有两个不同的源文件分别定义了以下全局变量和函数 源文件1:
int value; // 弱符号因为未初始化的全局变量
void foo() {} // 强符号因为是一个具体定义的函数源文件2:
int value 1; // 强符号因为是初始化的全局变量
void foo() {} // 强符号同样是一个具体定义的函数在链接这两个源文件时会发生以下情况
对于变量value源文件1中的是弱符号而源文件2中的是强符号。根据“强弱可以共存”的规则链接器会选择源文件2中的value。对于函数foo两个源文件中都是强符号。由于“一山不容二虎”的规则这将导致链接错误除非某种方式可以解决这个冲突比如静态链接库或者更改其中一个符号的名称。
2.2.2 GNU编译器的__attribute__((weak))属性 __attribute__((weak))是GNU编译器提供的一个功能强大的属性可以用于声明弱符号。这允许开发者为全局变量和函数提供默认的定义同时还可以被其他模块中的强符号覆盖。这在创建库和模块化编程时非常有用因为它允许用户扩展或修改默认行为而无需修改原始库代码。 为了深入理解如何在实际编码中应用强弱符号规则尤其是利用GNU编译器的__attribute__((weak))属性我们来扩展下上面的例子进一步示范这个特性的用法。
——举例如下 假设我们现在有三个源文件分别是main.clibdefault.c和userlib.c
libdefault.c是一个库文件提供了一些默认的实现。userlib.c是用户自定义的库可能会提供一些默认实现的替代版本。main.c是主程序将使用这些库提供的功能。
源文件 libdefault.c
#include stdio.hvoid __attribute__((weak)) foo() {printf(Default foo implementation\n);
}int __attribute__((weak)) value 42;这里foo函数和value变量都被声明为弱符号意味着如果存在同名的强符号它们可以被覆盖。
源文件 userlib.c (用户提供了自己的实现但这不是必须的)
#include stdio.hvoid foo() {printf(User-defined foo implementation\n);
}int value 100;在userlib.c中用户提供了foo函数和value变量的自定义实现不使用__attribute__((weak))因此这两个符号都是强符号。
源文件 main.c
#include stdio.hextern void foo();
extern int value;int main() {foo();printf(Value is %d\n, value);return 0;
}在main.c中主程序声明了对foo函数和value变量的外部引用并在主函数中调用foo和访问value。
——链接和运行结果
当我们将这三个文件一起编译和链接时
如果userlib.c被包含在编译过程中那么它里面的foo和value将作为强符号覆盖libdefault.c中的弱符号定义。如果userlib.c没有被编译链接那么libdefault.c中的弱符号实现将会被采用。
假设userlib.c被包含程序输出将是
User-defined foo implementation
Value is 100如果userlib.c未被包含输出将是
Default foo implementation
Value is 42这个例子展示了如何通过GNU编译器的__attribute__((weak))属性来设计灵活的接口和可扩展的程序结构允许易于维护和升级的同时也提供了默认的行为实现。
2.3 重定位 经过符号决议我们解决了链接过程中多文件符号冲突的问题。可执行文件的符号表中的每个符号虽然都确定下来 了但是符号表中的每个符号值也就是每个函数、全局变量的地址还是原来各个目标文件中的值还都是基于零地址的偏移。链接器将各个目标文件重新分解组装后各个段的起始地址已经发生了变化。当前重组后的文件中需要为这些符号指定新的地址这就是重定位过程的工作。 重定位是编译链接过程中的一个关键步骤它负责调整和转换目标文件中的符号引用确保程序中的每个符号引用都指向正确的内存地址。经过符号决议后代码和数据已经从它们最初的目标文件中被重新组织可能被放置在内存中的不同位置。因此链接器必须更新这些引用以反映它们在内存中的实际位置。
2.3.1 重定位的基本原理 当链接器合并多个目标文件时它会创建两个主要的表符号表和重定位表。符号表包含了所有符号的名称和它们的属性如是否为强符号或弱符号。在链接过程中符号决议会为每个全局符号赋予一个唯一的地址。重定位表则记录了所有需要更新内存地址的地方这些位置在源代码中通常是相对引用例如从函数的开头到一个全局变量的偏移量。 重定位表包含三个关键的信息
偏移Offset需要重定位的代码或数据位置相对于段开始的偏移。符号Symbol此重定位项对应到符号表中的符号。类型Type重定位类型指导链接器如何进行重定位。
2.3.2 重定位操作的流程 链接器进行重定位的基本步骤如下
确定基地址链接器为每个段代码段、数据段等确定一个基地址。遍历重定位表链接器遍历每个目标文件的重定位表查找需要修正的引用。计算新地址对于每个表项链接器根据符号表中的符号地址、重定位表中的偏移和确定的基地址来计算新地址。更新引用链接器将目标文件中的相对地址更新为实际的内存地址。
——举例如下 假设我们有两个简单的C文件main.c和library.c它们被编译成两个目标文件main.o和library.o。 源文件 library.c
int shared_val 10;void library_function() {// 函数实现
}源文件 main.c
extern int shared_val;int main() {return shared_val;
}编译这两个文件将产生目标文件这时shared_val的地址还没有最终确定。链接器通过重定位表来识别main.c中对shared_val的引用这个引用在main.o中是一个基于零地址的偏移。 当链接器将main.o和library.o链接成一个可执行文件时它可能决定将shared_val放在地址0x10000处。链接器会在main函数中重定位对shared_val的引用将它从偏移更改为实际地址0x10000。 通过符号表和重定位表的配合链接器能够解决多个目标文件中的符号冲突并确保所有符号引用在最终的执行文件中都正确指向它们的实际地址。重定位是确保代码和数据在内存中正确布置的关键过程没有它程序将无法正确运行。
三、结语 至此整个编译链接过程就结束了最终生成的目标文件就是可执行的目标文件了。实际上编译链接过程每一环节都有很多要注意和学习的地方本文只是简单的梳理了一个框架和比较重要的知识还需要在以后的学习中不断扩充相关知识。 快速学习嵌入式开发其他基础知识 返回专栏总目录 《嵌入式工程师自我修养/C语言》
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/82392.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!