【淘饭饭源码】【quartz调度器源码】【QN防撤回源码】binutils源码分析

来源:zmq源码使用

1.Linux下的交叉编译环境设置
2.充分理解Linux GCC 链接生成的源码Map文件
3.GNU 工具链开发入门(一):工具链介绍

binutils源码分析

Linux下的交叉编译环境设置

       é‡‡ç”¨äº¤å‰ç¼–译的主要原因在于,多数嵌入式目标系统不能提供足够的资源供编译过程使用,因而只好将编译工程转移到高性能的主机中进行。

       linux下的交叉编译环境重要包括以下几个部分:

       1.对目标系统的编译器gcc

       2.对目标系统的二进制工具binutils

       3.目标系统的标准c库glibc

       4.目标系统的linux内核头文件

       äº¤å‰ç¼–译环境的建立步骤

       ä¸€ã€ä¸‹è½½æºä»£ç  下载包括binutils、gcc、glibc及linux内核的源代码(需要注意的是,glibc和内核源代码的版本必须与目标机上实际使用的版本保持一致),并设定shell变量PREFIX指定可执行程序的安装路径。

       äºŒã€ç¼–译binutils 首先运行configure文件,并使用--prefix=$PREFIX参数指定安装路径,使用--target=arm-linux参数指定目标机类型,然后执行make install。

       ä¸‰ã€é…ç½®linux内核头文件

       é¦–先执行make mrproper进行清理工作,然后执行make config ARCH=arm(或make menuconfig/xconfig ARCH=arm)进行配置(注意,一定要在命令行中使用ARCH=arm指定cpu架构,因为缺省架构为主机的cpu架构),这一步需要根据目标机的实际情况进行详细的配置,笔者进行的实验中目标机为HP的ipaq-hp PDA,因而设置system type为SAX0,SAX0 Implementations中选择Compaq iPAQ H/H。

       é…ç½®å®Œæˆä¹‹åŽï¼Œéœ€è¦å°†å†…核头文件拷贝到安装目录: cp -dR include/asm-arm $PREFIX/arm-linux/include/asm cp -dR include/linux $PREFIX/arm-linux/include/linux

       å››ã€ç¬¬ä¸€æ¬¡ç¼–译gcc

       é¦–先运行configure文件,使用--prefix=$PREFIX参数指定安装路径,使用--target=arm-linux参数指定目标机类型,并使用--disable-threads、--disable-shared、--enable-languages=c参数,然后执行make install。这一步将生成一个最简的gcc。由于编译整个gcc是需要目标机的glibc库的,它现在还不存在,因此需要首先生成一个最简的gcc,它只需要具备编译目标机glibc库的能力即可。

       äº”、交叉编译glibc

       è¿™ä¸€æ­¥éª¤ç”Ÿæˆçš„代码是针对目标机cpu的,因此它属于一个交叉编译过程。该过程要用到linux内核头文件,默认路径为$PREFIX/arm-linux/sys-linux,因而需要在$PREFIX/arm-linux中建立一个名为sys-linux的软连接,使其内核头文件所在的include目录;或者,也可以在接下来要执行的configure命令中使用--with-headers参数指定linux内核头文件的实际路径。

       configure的运行参数设置如下(因为是交叉编译,所以要将编译器变量CC设为arm-linux-gcc): CC=arm-linux-gcc ./configure --prefix=$PREFIX/arm-linux --host=arm-linux --enable-add-ons 最后,按以上配置执行configure和make install,glibc的交叉编译过程就算完成了,这里需要指出的是,glibc的安装路径设置为$PREFIXARCH=arm/arm-linux,如果此处设置不当,第二次编译gcc时可能找不到glibc的头文件和库。

       å…­ã€ç¬¬äºŒæ¬¡ç¼–译gcc

       è¿è¡Œconfigure,参数设置为--prefix=$PREFIX --target=arm-linux --enable-languages=c,c++。

       è¿è¡Œmake install。

       åˆ°æ­¤ä¸ºæ­¢æ•´ä¸ªäº¤å‰ç¼–译环境就完全生成了。

       å‡ ç‚¹æ³¨æ„äº‹é¡¹

       ç¬¬ä¸€ç‚¹ã€åœ¨ç¬¬ä¸€æ¬¡ç¼–译gcc的时候可能会出现找不到stdio.h的错误,解决办法是修改gcc/config/arm/t-linux文件,在TARGET_LIBGCC2_CFLAGS变量的设定中增加-Dinhibit_libc和-D__gthr_posix_h。

充分理解Linux GCC 链接生成的Map文件

       简单来说,map文件就是分析通过编译器编译之后,生成的源码程序、数据及IO空间信息的分析一种映射文件,里面包含函数大小,源码入口地址等一些重要信息。分析淘饭饭源码从map文件我们可以了解到:

       生成map文件是源码链接器ld的功能,有两种方式可以生成map文件:

       使用GNU binutils,分析必须通过设置正确的源码标志来显式地请求生成映Map文件。使用LD将Map打印到输出到output.map:

       作为一个简单程序的分析例子,你可以使用以下命令链接编译单元:

       为什么要了解Map文件:

       在本文中,源码我想突出说明链接器Map文件是分析多么简单,以及它可以教给我们关于正在处理的源码程序的一些知识。

       固件工程师很少在调试时使用构建过程生成的分析Map文件。然而,源码答案有时就在这个Map文件中。

       Map文件提供了有价值的信息,可以帮助您理解和优化内存。我强烈建议为在生产环境中运行的任何固件保存该文件。

       Map文件是整个程序的符号表。让我们深入研究它,看看它有多简单,以及如何有效地使用它。我将尝试用一些例子来说明,quartz调度器源码这些例子都是用GNU binutils来描述的。

       LED闪烁程序:

       还有什么比我们的老朋友LED闪烁程序示例解释一下Map文件的基础知识更好的呢?

       为了了解Map文件,我们使用Nordic SDK中的LED闪烁程序来编译,并修改它以添加对atoi的调用。然后,我们将使用Map文件来分析这两个程序之间的差异。

       下面就是示例的main.c文件:

       编译:

       生成的Map文件有多行 ,尽管它只是在闪烁发光二极管。这么多行不可能看不见,里面一定有一些重要的信息……

       现在让我们修改程序,添加对atoi的调用,我们不直接使用整数作为延迟函数的参数,而是将其编码为字符串并使用atoi解码,然后作为参数传给延时函数。

       经过编译,整个程序从字节变成了字节。

       我们能够想到调用atoi会带来更多的代码,但是%程序大小的增加是巨大的!

       深入研究Map文件:

       在下面的部分中,我将使用代码片段来解释Map文件的不同部分。

       Archives linked:

       下面是Map文件的第一行内容:

       上述信息的格式如下:

       上面内容的意思是crt0这个文件中会调用exit函数,exit函数在exit.o这个目标文件中,exit.o目标文件是被链接在libc_nano.a这个库文件里的。

       为什么是QN防撤回源码这样,不在本文的讨论范围内,但是你的工具链(这里是GNU工具链)确实提供了一些标准库。它们可用于提供atoi等标准功能,在这个例子中,我指定链接器使用nano.spec文件,这就是为什么标准函数都来自libc_nano.a。

       现在,比较两个生成的map文件,发现的第一个区别是程序中包含了一些其他的存档成员:atoi,它本身需要_strtol_r,_strtol_r本身又需要_ctype_:

       现在,我们对实际包含在程序中的文件以及它们存在的原因有了更好的理解。让我们来看看这个文件里面还有什么!

       Memory configuration:

       Map文件中最直接的信息是实际的内存区域,这些区域具有位置、大小和访问权限:

       Linker script and memory map:

       内存配置之后是Linker script and memory map,这个很有趣,因为它给出了程序中符号的详细信息。在我们的例子中,它首先指示text区域的大小及其内容(text是我们编译的代码,而data是程序数据)。

       在这里,中断向量(在.isr_vector下)出现在可执行文件的开头,定义在gcc_startup_nrf.S中:

       这些行给出了每个函数的优选源码代下载地址和大小。在上面,你可以读取bsp_board_led_invert的地址,它来自boards.c.o(如你所猜测的,board.c的编译单元),在text区域中它的大小为0x字节。这样,我们就可以定位程序中使用的每个函数。

       我的常量字符串_delay_ms_str在程序初始化时显然包含在程序中,只读数据作为rodata保存在链接器脚本中指定的FLASH区域中(存储在Flash中,而不是复制在RAM中,因为它是常量)。我可以在这行下面找到:

       我还注意到_ctype_的包含在text区域中增加了0x字节的只读数据

       标准库是开源的( 链接),我们很容易就能找出它占用那么多空间的原因。我深入到atoi的内部(可重入版本的atoi_r,见下文),它是直接调用的strtol_r:

       对于strtol_r,它实际上比仅仅将字符转换为整数更复杂,因为还使用ctype来执行类型检查。ctype的工作方式是使用一个表,其中ASCII符号类型存储在一个数组中。下面是ctype的主要部分,并附上我的注释:

       有趣的是,atoi的进攻吸筹源码添加不仅增加了代码的大小(text区域),还增加了数据的大小(data区域)。分析两个Map文件,我可以很容易地发现之前被链接器丢弃的数据:

       现在你可能已经注意到函数名以_r结尾,例如在调用strtol_r时,该后缀表示可重入性。有关可重入性的文档可以在 newlib源代码中找到。总而言之,即使同一函数已经在另一个进程中执行,也可以调用可重入函数,而不会干涉执行。从文档中可以看到如下描述:

       Each function which uses the global reentrancy structure uses the global variable _impure_ptr, which points to a reentrancy structure.

       在我们的例子中,我们需要新的全局变量来调用可重入函数:atoi_r。

       最后要记住的一点信息是:初始化变量必须保存在Flash中,但它们在Map文件中会出现在RAM中,因为它们在进入主函数之前被复制到RAM中。在这里,符号__data_start__和__data_end__跟踪RAM中用于保存初始化变量的区域,这些值存储在Flash中,起始位置为0xd0:

       Discarded sections:

       如果链接器没有找到对函数和变量的任何引用,编译后包含在程序中的函数和变量并不总是最终二进制文件的一部分,它们将会被删除但是仍然会出现在Map文件的Discarded input sections 部分。例如,下面是一些定义在boards.c中的函数,它们永远不会被调用并因此被丢弃:

       Common symbols:

       这个部分没有出现在我们的Map文件中,但它值得一提。

       Common symbols(通用符号)是可以在代码中的任何地方使用的非常量全局变量(non-constant global variables)。您可能知道,使用全局变量通常不是一个好的实践,因为它们使代码更难维护。确实如此,作用域是全局的,每个外部模块可以修改任何全局变量的值,访问时必须考虑到这一点。将变量隔离到一个模块中,使用static关键字,通常更好地确保创建变量的模块完全负责其状态。

       现在,如果您希望使程序更安全并防止访问某些全局变量,请查看Map文件部分。如果某些变量不需要声明为全局变量,您可能希望将它们转换为静态变量。

       Map文件有几种可能的用法:大多数时候,一个地址后面对应着一个函数,我们希望通过这个地址去了解一些问题。例如,它可以是硬故障处理程序(Hard Fault handler)中的程序计数器(Program Counter)。其他时候,你也会遇到调试一些不明确的行为,最终发现你的程序意外地写入了一个出界数组(数组越界)。当有了ELF文件时,arm-none-eabi-nm对于这些事情也非常有用,它提供了按大小排序符号的选项。

       但有些时候,它甚至在你有可执行文件之前就很有用了……

       Debugging a linking error:

       Map文件是在构建代码(.o文件)链接在一起的时候生成的,这意味着它可以有助于解决链接过程中出现的错误。我记得在几个Flash页面中包含一个引导加载程序,在某些情况下,我想使用atoi,但引导加载程序不再编译,因为没有更多的可用空间。

       使用前面的示例,假设我现在只有0x字节的Flash。编译第一个示例时,如果没有atoi,就不会出现问题,但是第二个例子会溢出我们的Flash:

       是不是很讨厌?atoi只是一个很简单的函数而已,居然就出现这种问题。但正如我们前面所提到的,使用libc_nano.a需要比预期更多的Flash空间。

       让我们来实现自己版本的atoi,其实也没那么难。以下是编译后的结果(config CUSTOM_ATOI):

       这个方法是不是很好?现在可以将代码塞进0x字节的Flash,以满足我们的(假)需求。

       分析Map文件可以让我们了解很多正在编写的代码,这是改进固件的第一步。

       可以使用一些工具来解析Map文件并获得程序的汇总视图,后面有时间和大家好好聊聊。

GNU 工具链开发入门(一):工具链介绍

       进入GNU工具链开发领域,首先需认识到GNU/Linux世界中,GNU工具链扮演着不可或缺的角色。尽管在代码耦合程度和可读性方面,GNU工具链可能不如使用Apache协议的LLVM广受欢迎,但只要GNU/Linux系统持续存在,GNU工具链的使用不会停止。作者自年开始深入探索GNU工具链开发,并通过系列文章整理知识体系,分享个人经验。

       GNU工具链主要由三个部分构成:GCC、GAS和LD。GCC作为编译器集合,包含多种编译器,如用于C语言的GCC、用于C++的G++等,而GAS是汇编器,负责将汇编代码转换为机器代码。LD则是链接器,将生成的模块链接成可执行文件或共享库。

       在GCC的使用中,编译过程可以被详细拆解。一个简单的示例为使用`gcc hello.c -o hello`命令编译hello.c文件并生成hello程序。这里实际涉及多个步骤:首先,GCC调用C语言编译器cc1将源代码编译为汇编代码;随后,GAS将汇编代码转换为对象文件;最后,LD链接库文件,生成最终的可执行文件。

       在构建程序时,GCC通过调用AS和LD完成汇编和链接过程,具体参数的传递可能依据不同后端的设置。此外,BFD(Binary File Descriptor)是Binutils仓库中用于底层操作的工具集,包括as、ld等。

       GLibc作为GNU C Library,在构建GNU/Linux和Linux内核时扮演关键角色。它包含几乎所有的UNIX标准,对构建过程至关重要。作者对GLibc的理解仍停留在浅层阶段,专注于RISC-V后端相关工作,对C库的具体实现不敢妄加评论。

       构建GCC的过程通常基于上游最新代码进行,步骤包括配置、编译、链接和安装等。具体步骤可参考相关指南或教程。构建GCC时需要注意版本兼容性、依赖库以及构建环境的设置。

       总结,GNU工具链作为GNU/Linux生态系统的核心组成部分,为用户提供强大的编译和链接功能。了解其构成、使用方法以及构建流程,对深入参与GNU/Linux生态系统的开发工作至关重要。

文章所属分类:时尚频道,点击进入>>