ARM链接脚本
对load_start,load_end, run_start汇编伪指令的理解
`load_start`, `load_end`, 和 `run_start` 是 ARM 汇编伪指令,通常用于描述一个代码段的加载和运行开始/结束位置。
这些伪指令通常在嵌入式系统或低级系统编程中使用,以帮助链接器或加载器确定如何加载和运行代码。
1. load_start: 这个伪指令标记了代码段的开始位置,这个位置是在程序被加载到内存中时确定的。
这个标签通常用于确定程序在内存中的基地址。
2. load_end: 这个伪指令标记了代码段的结束位置。
这个标签可以帮助确定程序的大小,从而可以在加载时为其分配足够的内存。
3. run_start: 这个伪指令标记了代码段的运行开始位置。
这通常是在程序开始执行之前,由链接器或加载器确定的地址。
这个地址通常会被用作程序的入口点。
这些伪指令通常在链接脚本中使用,链接脚本是用来描述如何链接程序的各个部分的文件。
通过使用这些伪指令,链接器或加载器可以确定程序在内存中的位置,并正确地执行它。
需要注意的是,这些伪指令的行为可能会根据不同的链接器和加载器而有所不同,因此具体行为可能需要参考相关工具的文档或手册。
链接脚本(LinkerScript)应用实例(一)使用copytable将函数载入到RAM中运行
链接脚本(LinkerScript)应⽤实例(⼀)使⽤copytable将函数载⼊到RAM中运⾏将函数载⼊到RAM中运⾏需要以下三个步骤:(1)⽤编译器命令#pragma section "<section name>" <user functions> #pragma section 将想要载⼊RAM运⾏的函数存储为⾃定义段名的程序段,其中ax是#pragma section命令中的可选设置——<flags>,a表⽰allocatable,x表⽰executable,具体#pragma section ".flash_driver" axvoid PFlashProgram( uint32 flash, uint32 addr, uint32 word_l, uint32 word_u ){uint32 load_cnt;uint16 endinitSfty_pw = IfxScuWdt_getSafetyWatchdogPasswordInline();IfxFlash_enterPageMode(addr);/* wait until unbusy */IfxFlash_waitUnbusy(flash, IfxFlash_FlashType_P0);/* write 32 bytes (8 doublewords) into assembly buffer */for (load_cnt = 0; load_cnt < 4; load_cnt++){IfxFlash_loadPage2X32(addr, word_l, word_u);}/* write page */IfxScuWdt_clearSafetyEndinitInline(endinitSfty_pw);IfxFlash_writePage(addr);IfxScuWdt_setSafetyEndinitInline(endinitSfty_pw);/* wait until unbusy */IfxFlash_waitUnbusy(flash, IfxFlash_FlashType_P0);}#pragma section(2)在链接脚本中,以⾃定义的程序段为输⼊,定义输出段output section: /* user defined output section, used to allocate ram space(VMA) for code running in ram, *//* and also allocate rom space(LMA) to store it */.code2ram :{*(.flash_driver)*(.flash_driver.*). = ALIGN(8);} > psram_local AT> pfls0 =0(3)将该输出段.code2ram的LMA、VMA和SIZE等信息加⼊到copy table中,以使CPU启动时将该段(函数)从ROM拷贝到RAM中(从⽽正常运⾏)。
链接脚本语言的编写
0. Contents1. 概论2. 基本概念3. 脚本格式4. 简单例子5. 简单脚本命令6. 对符号的赋值7. SECTIONS命令8. MEMORY命令9. PHDRS命令10. VERSION命令11. 脚本内的表达式12. 暗含的连接脚本1. 概论编译的过程得到的obj文件,是完全和地址无关的,例如,每个源文件对应的obj文件,地址都是从0开始的,它真正连接的时候才由连接器linker分配真正运行时候的地址,所以,你要想处理每个具体的代码段运行的时候应该在什么地方,就要注意学习link了。
每一个链接过程都由链接脚本(linker script, 一般以lds作为文件的后缀名)控制. 链接脚本主要用于规定如何把输入文件内的section放入输出文件内, 并控制输出文件内各部分在程序地址空间内的布局. 但你也可以用连接命令做一些其他事情.连接器有个默认的内置连接脚本, 可用ld --verbose查看. 连接选项-r和-N可以影响默认的连接脚本(如何影响?).-T选项用以指定自己的链接脚本, 它将代替默认的连接脚本。
你也可以使用<暗含的连接脚本>以增加自定义的链接命令.以下没有特殊说明,连接器指的是静态连接器.2. 基本概念链接器把一个或多个输入文件合成一个输出文件.输入文件: 目标文件或链接脚本文件.输出文件: 目标文件或可执行文件.目标文件(包括可执行文件)具有固定的格式, 在UNIX或GNU/Linux平台下, 一般为ELF格式. 若想了解更多, 可参考UNIX/Linux平台可执行文件格式分析有时把输入文件内的section称为输入section(input section), 把输出文件内的section称为输出section(output sectin).目标文件的每个section至少包含两个信息: 名字和大小. 大部分section还包含与它相关联的一块数据, 称为section contents(section内容). 一个section可被标记为“loadable(可加载的)”或“allocatable(可分配的)”.loadable section: 在输出文件运行时, 相应的section内容将被载入进程地址空间中. allocatable section: 内容为空的section可被标记为“可分配的”. 在输出文件运行时, 在进程地址空间中空出大小同section指定大小的部分. 某些情况下, 这块内存必须被置零.如果一个section不是“可加载的”或“可分配的”, 那么该section通常包含了调试信息. 可用objdump -h命令查看相关信息.每个“可加载的”或“可分配的”输出section通常包含两个地址: VMA(virtual memory address 虚拟内存地址或程序地址空间地址)和LMA(load memory address加载内存地址或进程地址空间地址). 通常VMA和LMA是相同的.在目标文件中, loadable或allocatable的输出section有两种地址: VMA(virtual Memory Address)和LMA(Load Memory Address). VMA是执行输出文件时section所在的地址, 而LMA是加载输出文件时section所在的地址. 一般而言, 某section的VMA == LMA. 但在嵌入式系统中, 经常存在加载地址和执行地址不同的情况: 比如将输出文件加载到开发板的flash中(由LMA指定), 而在运行时将位于flash中的输出文件复制到SDRAM中(由VMA指定).可这样来理解VMA和LMA, 假设:(1) .data section对应的VMA地址是0x08050000, 该section内包含了3个32位全局变量, i、j和k, 分别为1,2,3.(2) .text section内包含由"printf( "j=%d ", j );"程序片段产生的代码.连接时指定.data section的VMA为0x08050000, 产生的printf指令是将地址为0x08050004处的4字节内容作为一个整数打印出来。
ARM-GCC-LD脚本
ARM-GCC-LD脚本从以前的经验,链接脚本是嵌⼊式开发,单⽚机开发相当重要的⼀个东西。
它完成的⼯作是做PC机软件的同志们不⽤关⼼的,但是也是很复杂的⼀项⼯作。
总结来看链接脚本要告诉连接器1:输出什么2:输⼊是什么,那么obj⽂件3:要⽤什么库,库放在什么地⽅4:内存分布地址5:提供启动代码⼀些全局地址变量⼀般来说链接脚本需要搞清楚这⼏样事情后才能编写,那arm-gcc-ld的脚本也⼀定要实现这些功能。
对于⼤多数的链接器来说,对于简单的项⽬不需要脚本,只是使⽤命令参数就可以完成了。
MEMORY:它是⽤来补充SECTIONS命令的,⽤来描述⽬标CPU中可⽤的内存区域。
它是可选的,如果没有这个命令,LD会认为SECTIONS描述的相邻的内存块之间有⾜够可⽤的内存。
其实很容易理解但是却很少⽤(我没⽤过,嘿嘿),在SECTIONS中每个段的分布都没有考虑ARM能够寻址的地址中,ROM,RAM,FLASH是不是连续的。
如果不是连续的怎么办?MEMORY就是设置各个区的起始位置,⼤⼩,属性的命令,在⼀个脚本中只能有⼀个。
举⼀个例⼦:如果你的板⼦有两段存储,⽽且很遗憾的是不是连续的,⼀段是从0x0开始,⼤⼩为256K,另⼀段是从0x40000000开始的⼤⼩为4M,你可以在脚本中写⼊如下的代码来描述你的板⼦的内存信息。
1 MEMORY2 {3 rom (rx) : ORIGIN = 0, LENGTH = 256K4 ram (!rx) : org = 0x40000000, l = 4M5 }很显然下⾯的⼀句⽤了简略标签,这并不重要,重要的是怎样使⽤它,不过在那之前还是想再仔细研究下MEMORY命令的细节。
MEMORY命令的语法是:MEMORY{name (attr) : ORIGIN = origin, LENGTH = len...}name:⼀个⽤户定义的名字,Linker将在内部使⽤它,所以别把它和SECTIONS⾥⽤到的⽂件名,段名等搞重复了,它要求是独⼀⽆⼆的。
03 S32K1X系列基于SDK的启动文件连接脚本分析
近期在使用S32K144的ARM CM4F内核的片子做项目,开发IDE使用的是NXP官方的S32DS,其开发模式和MDK类似,但是可配置性更灵活一些,但是IDE也为我们做了大量的工作,启动文件/连接脚本这些都给我们提供了,所以当我们在线调试时,会直接跳转到main()函数,下面我们来看一下IDE都为我们做了那些工作:一.分析一下启动文件1,启动文件在新建工程后的Project_settings目录下(1)定义了.section.isr_vector,"a"段,这个段即中断向量表;(2).align2表示下面的代码要4自己解对齐,并将符号__isr_vector声明为全局变量,以便连接脚本调用,值为0x00000000(3)接下来就是中断向量表的排布了,前连个比较特别,一个是SP,一个是复位向量,这和ARM内核的特性有关,不多说;(4)这个向量表的大小是多少呢,256个向量,每个占4字节,即0x400;2,定义了一个段.section.FlashConfig,"a"(1)这个段是关于flash配置参数的,仅仅定义了4个数值,后面会用来配置flash3,接下来,就该text段了(1)首先表明text段的组织方式,使用thumb指令4,接下来,就是真正的代码段了,即复位函数,同时把该函数的名字/或是地址声明为全局变量,也是为了在连接脚本中使用5,分析一下,复位函数:(1)关闭全局中断,这是必须的,因为刚开始你什么都没配置;并清除了通用寄存器(2)如果定义了START_FROM_FLASH,就要初始化ECC RAM这个宏定义在哪里定义呢,我没有仔细查看,应该是根据我们工程配置来定义的,可能是根据我们的连接脚本的选择是FLASH/RAM的方式来定义该变量,因为我在.cproject文件中找到了该宏的定义,暂时不深究,我们分析一下这个代码段做了什么:A,为什么我从flash启动就需要初始化RAM呢,这个RAM是片内的RAM,还需要我初始化吗,查了些资料没有好的解释,暂时先不管了,反正就是为了在你使用内部RAM前的一个初始化操作,保障你后面的使用;B.获取RAM的起始/结束地址,分别放入寄存器r1/r2,这连个地址的定义在连接脚本中具体的值我们在对应工程生成的map文件中可以找到上面的BLE指令时根据前面的执行结果来决定跳转的,如果前面的结果为假就调转,否则就跳过BLE指令,继续顺序执行下面的指令,这里我们的RAM空间肯定不为空,所以不会执行BLE;C.接着执行LC4的函数内容,这里就是init ECC RAM的内容了,细节就不分析了;(3)初始化堆栈,这个比较简单了,获取符号__StackTop的地址,然后赋值给r13,即SP(4)接下来调用SDK提供的系统初始化函数,这个函数定义在system_S32K144.c文件中,主要做了系统相关的初始化主要就是做了FPU/WDOG等相关的一些初始化;注意一点:这里在调用这个C函数之前,已经把SP堆栈给设置好了,这也说明了C函数的运行,必须有堆栈的初始化为前提;(5)接下来,就是启动文件的核心函数了,data的搬移,bss段的初始化,开中断,跳转到main()函数a.Init_data_bss函数定义在SDK提供的startup.c文件中b.Init_data_bss函数是核心函数,我们需要详细分析一下:1,首次我们使用的编译器时GCC的,所以只关心gcc相关的一些定义2,定义了一堆指针,用来记录数据部分的指针,这些值的提取来自于连接脚本;下面的3个变量定义在连接脚本中,且这里以数组的形式定义,因为在连接脚本中这些变量就是一个地址下面的几个变量也是在连接脚本中定义的,这里用法和上面的是一样的,接下来就对一开始定义的一些变量进行初始化了,他们用来控制接下来的数据搬迁工作:这些变量的值我们在下面进行列举,具体的值我直接给出,有疑问的请查看对应工程的map文件data_ram=(uint8_t*)__DATA_RAM=0x1fff8400data_rom=(uint8_t*)__DATA_ROM=0x000044d0data_rom_end=(uint8_t*)__DATA_END=0x00008a94code_ram=(uint8_t*)__CODE_RAM=0x1fffc9c4code_rom=(uint8_t*)__CODE_ROM=0x00008a94code_rom_end=(uint8_t*)__CODE_END=0x00008af0bss_start=(uint8_t*)__BSS_START=0x20000000bss_end=(uint8_t*)__BSS_END=0x2000004ccustom_ram=CUSTOMSECTION_SECTION_START=0x20000000custom_rom=(uint8_t*)__CUSTOM_ROM=0x00008af0custom_rom_end=(uint8_t*)__CUSTOM_END=0x00008af0上面已经把需要获取的数据的细节地址得到了,接下来就要搬迁数据了:把初始化了的数据从ROM搬移到RAM,size=0x454c=17K靠,这么大,因为我的boot是测试用的,在里面定义了一个数组来存放APP的程序数据,所以才会这么大;另外,搬到RAM的0x1fff8400地址,也是有原因的,因为前面的0x400是要留给中断向量表的;拷贝用户使用特殊宏定义的代码段拷贝到RAM中运行,我们这个工程好像就把flash的操作命令序列函数给搞到RAM中运行了,所以看一下size=0x5c=92字节;看来就是flash操作的一个函数给搬迁到RAM了;Bss段是存储初始化为0,或没有初始化的变量,所以在flash代码中不会存放一大堆0,所以这里只需要根据bss段的起始地址和大小来初始化对应空间为0即可,size=0x4c=76字节;拷贝用户定义的段,这里我们没有使用用户自定义的段,所以size=0这个操作直接跳过接下来,获取一下内核ID,#define GET_CORE_ID()0U所以cordid=0;最后一步,也很重要,就是判断中断向量表是否要搬迁到RAM的的操作,这个配置需要结合工程的配置来决定,例如:如果你使用了SDK建立工程,肯定要搬迁中断向量表到RAM,因为SDK中会动态的安装和更改外设的中断函数;__VECTOR_RAM=0x1fff8000__VECTOR_TABLE=0x00000000__RAM_VECTOR_TABLE_SIZE=0x00000400不相等啊,肯定要搬移中断向量表了,而且大小是0x400;最后更改向量表寄存器的值,即指向新的向量表的地址,以便中断发生时去正确的地方获取向量;3,最后,打开NVIC的中断,开始响应中断,跳转到main()6,在.S文件的后面就是中断函数的实现了,这里也仅仅是定义了空的中断处理函数,而且,也声明为week;即外部定义的中断处理函数会覆盖这里的空函数;至此,.S函数分析完了;二.分析一下连接文件,我们一般常用的是在flash中链接的脚本,所以直接分析;1,连接文件简单些,首先就是定义了堆栈的尺寸,并且指定Reset_Handler为连接的起始函数2,定义中断向量表的尺寸3,接下来,内存mem的分配,中断向量表的起始地址和尺寸;最主要的是text段的空间,即除了中断向量表和flash配置的区域外,剩下的全是text的;另外,就是RAM的区域划分了,这个就是根据cpu内部的铺设来设置的,不多说了;4,接下来,是核心的数据段的具体划分以及分布链接段的格式是:SECTIONS{name:{段内容}>链接到那个分区}5,详细的就自己看吧,里面会跟随连接地址顺序,定义很多变量,这些变量的值就是我们在上面数据搬移是用到的一些全局地址变量;补充一点:__stack_start__=.;表示将当前地址值赋值给变量__stack_start__。
arm链接文件规则(mynote)
Arm中的链接文件的规则-T选项是ld命令中比较重要的一个选项,可以用它直接指明代码的代码段、数据段、博士生、段,对于复杂的连接,可以专门写一个脚本来告诉编译器如何连接。
-Ttext addr-Tdata addr-Tbss addrarm-elf-ld -Ttext 0x00000000 -g led_On.o -o led_on_elf ,运行地址为0x00000000,由于没有data和bss,他们会默认的依次放在后面。
相同的代码不同的Ttext,你可以对比一下他们之间会变的差异,ld会自动调整跳转的地址。
*简单的Linker script(1) SECTIONS命令:The SECTIONS command tells the linker how to map input sections into output sections, and how to place the output sections in memory.命令格式如下:SECTIONS{sections-commandsections-command......}其中sections-command可以是ENTRY命令,符号赋值,输出段描述,也可以是overlay描述。
(2) 地址计数器‘.’(location counter):该符号只能用于SECTIONS命令内部,初始值为‘0’,可以对该符号进行赋值,也可以使用该符号进行计算或赋值给其他符号。
它会自动根据SECTIONS命令内部所描述的输出段的大小来计算当前的地址。
(3) 输出段描述(output section description):前面提到在SECTIONS命令中可以作输出段描述,描述的格式如下:section [address] [(type)] : [AT(lma)]{output-section-commandoutput-section-command...} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]很多附加选项是用不到的。
ARM汇编指令集
ARM汇编指令集汇编指令集的介绍,包括指令和伪指令。
指令和概念指令指令指的是CPU机器指令的助记符,是由CPU的指令集提供的,经过编译之后,会以机器码的形式由CPU读取执⾏伪指令伪指令本质上不是指令,和CPU的机器指令没有任何关系,只是和指令⼀起写在代码中⽽已,是由环境提供的,其⽬的是⽤于指导编译过程,伪指令经过编译后不会⽣成⼆进制机器码,仅仅在编译阶段有效果指令编程风格ARM官⽅风格官⽅风格指令⼀般使⽤⼤写,例如:LDR R0,[R1],Windows中常使⽤这种风格GUN Linux风格指令⼀般使⽤⼩写字母,例如:ldr r0,[r1],Linux环境中常⽤这种风格ARM汇编特点LDR/STR架构1. 采⽤RISC架构,CPU本⾝不能直接读取内存,⽽需要把内存中的数据加载到CPU的通⽤寄存器中,才能被CPU处理2. ldr(load register)将内存中的数据加载到通⽤寄存器3. str(store register)将寄存器内容存⼊内存空间4. ldr和str组合,可以实现ARM CPU和内存的数据交换8种寻址⽅式1. 寄存器寻址:move r1,r2:把r2的值赋值到r1寄存器中2. ⽴即寻址:move r0,#0xFF00:把⽴即数0xFF00赋值给r0寄存器3. 寄存器移位寻址:move r0,r1,lsl #3:把r1左移三位(*8)之后的值赋值给r0寄存器4. 寄存器间接寻址:ldr r1,[r2]:寄存器有中括号,表⽰内存地址对应的数据,所以这⾥r2表⽰⼀个内存地址,[]表⽰取r2指针对应的数据,这句代码的意思是把r2对应的内存中的数据赋值给r15. 基址变址寻址:ldr r1,[r2,#4]:将指针r2的值(内存地址)+4之后指向的数据赋值给r16. 多寄存器寻址:ldmia r1!,{r2 - r7,r12}:这种情况下,r1是⼀个指针,⾥边存放的内存地址,然后以r1⾥边的内存地址为基地址,向后以此加1得到{}⾥的寄存器数量个内存地址,然后将刚才得到的这些内存地址指向的变量的值赋值给{}⾥的对应位置的寄存器,类似从内存中读取数组,然后把数组的元素依次赋值给这些寄存器7. 堆栈寻址:stmfd sp!,{r2 - r7,lr}:和多寄存器类似,区别是将栈SP中连续访问{}数量个字节,然后依次赋值给{}⾥的寄存器8. 相对寻址:beq flag::flag:标号⽤于标记标号后⾯那句指令的地址,常⽤来表⽰⼊⼝点,函数名就是⼀个标号,C语⾔中的goto就可以跳转到⼀个标号,在ARM汇编中⽤指令b flag:就可以跳转到flag:对应的标号处执⾏,和beq flag:是⼀样的,其原理是相对于PC程序位置寄存器做⼀个偏移指令后缀1. ARM中的指令可以带后缀,从⽽丰富该指令的功能,这种形式叫做指令族,常⽤的后缀有:2. B(byte):功能不变,操作长度变为8位(依赖CPU位数,以下相同)3. H(Halfword):功能不变,操作长度变为16位3. H(Halfword):功能不变,操作长度变为16位4. S(signed):功能不变,操作数变为有符号数5. S(S标识):影响CPSR⾥的NZCV标识位,6. 举例:1. ldr指令族:ldrb,ldrh,ldrsb ldrsh,从内存中加载指定长度的数据2. mov指令族:movs r0,#0,结果是0,赋值会影响CPSR的NZCV标识,将Z位置为1条件执⾏后缀1. 条件执⾏后缀⽤于限制该执⾏执⾏的,只有在符合条件之后才能够执⾏该指令2.3. 举例:moveq r0,r1,如果eq成⽴,执⾏mov r0,r1,不成⽴则该条不执⾏,和C语⾔中的条件判断类似4. 条件后缀成⽴与否,不是取决于本条指令,⽽是取决于之前指令运⾏后的结果5. 条件后缀决定了本条指令是否执⾏,不会影响之前和之后指令6. 条件后缀和CPSR的NZCV位相关,例如,如果上⼀句代码执⾏的结果将Z置为1,下⼀句带有eq条件后缀的语句就会被执⾏多级指令流⽔线1. 多级流⽔线⽤于增加处理器处理指令的速度,2. 允许CPU同时异步的执⾏多条指令,⽽⾮上⼀条指令全部执⾏完毕之后才会执⾏下⼀条指令3. 多级可以简单那理解为把⼀条指令分为多个步骤来异步执⾏,例如:1. CPU把⼀条指令分为[取址,解码,执⾏]3个步骤,则为3级指令流⽔线2. 第⼀条指令进⾏取值操作3. 第⼀条指令取值完毕,进⼊解码操作,第⼆条指令紧随其后就开始执⾏取值操作4. 第⼀条指令解码完毕,进⼊执⾏操作,第⼆条指令紧接着进⼊解码操作,同时第三条指令进⼊取值操作5. 第⼀条指令执⾏完毕,第⼆条指令进⼊执⾏操作,第三条指令进⼊解码操作,第四条指令进⼊取值操作,依次类推4. 可见,多级流⽔线可以提⾼同时执⾏指令的数量,从⽽加速指令执⾏5. 需要注意的是,PC指向的是正在取值的指令,⽽⾮正在执⾏的指令,之间的差值就是流⽔线级数和单字节长度的乘积,在中断返回到PC的时候需要注意这个问题ARM指令数据处理指令数据传输指令mov:move,在两个寄存器之间或者⽴即数和寄存器之间传递数据,将后⼀个寄存器上的值或者⽴即数赋值给前⼀个寄存器 例如:mov r1,r0mov r1,#0xFF:将⽴即数0xFF赋值给寄存器r1mvn:和mov⽤法⼀致,区别是mvn会把后⼀个寄存器的值或者⽴即数按位取反后赋值给前⼀个寄存器 例如:mvn r0,#0xFF,则r0的值为0xffffff00(32位数据)算术运算指令add:加法运算sub:减法运算rsb:反减运算adc: 带进位的加法运算sbc: 带进位的减法运算rsc:带进位的反减指令逻辑指令and:与操作orr:或操作eor:异或操作bic:位清除操作⽐较指令cmp:⽐较⼤⼩cmn:取反⽐较tst:按位与运算teq:按位异或运算乘法指令mvl: mla: umull: umlal: smull: smlal:前导0计数clz:统计⼀个数的⼆进制位前⾯有⼏个0CPSR访问指令mrs⽤于读取CPSR和SPSRmsr⽤于写CPSR和SPSRCPSR和SPSRCPSR是程序状态寄存器,整个Soc只有⼀个SPSR在五种异常模式下各有⼀个,⽤于从普通模式进⼊异常模式的时候,保存普通模式下的CPSR,在返回普通模式时可以恢复原来的CPSR跳转分⽀指令b指令: ⽆条件直接跳转,没打算返回bl指令:跳转前把返回地址放⼊lr中,以便返回,常⽤在函数中bx指令:跳转同时切换到ARM模式,⽤于异常处理的跳转内存访问指令ldr:加载指定内存地址的数据到寄存器,按照字节访问str:加载指定寄存器数据到内存地址中,按照字节访问ldm:和ldr功能⼀样,⼀次多字节多寄存器访问stm:和str功能⼀样,⼀次多字节多寄存器访问swp:内存和寄存器互换指令,⼀边读⼀边写,例如:swp r1,r2,[r0]:读取指针r0的数据到r1中,同时把r2的数据赋值给r0指针指向的变量软中断指令swi(software interrupt),在软件层模拟产⽣⼀个中断,这个中断会传送给CPU,常⽤于实现系统调⽤⽴即数⾮法与合法ARM指令都是32为,除了指令标记和操作标记外,只能附带少位数的⽴即数,所以有⾮法与合法之分⾮法⽴即数:合法⽴即数:经过任意位数的移位后,⾮0部分可以⽤8位表⽰就是合法⽴即数协处理器与指令协处理器协处理器属于Soc中另外⼀颗核⼼,⽤于协助主CPU实现某些功能,被主CPU调⽤来执⾏任务,协处理器和MMU,Cache,TLB有功能和管理上的联系ARM设计可以⽀持多达16个协处理器,但是⼀般只实现其中的CP15协处理器指令mrc:读取CP15中的寄存器mcr:向CP15中的寄存器写数据指令⽤法:mcr{<”cond”>} p15,<”opcode_1”>,<”Rd”>,<”Crn”>,<”Crm”>,{<”opcode_2”>} opcode_1:对于CP15永远为0Rd:ARM通⽤寄存器Crn:CP15寄存器,取值范围c0~c15Crm:CP15寄存器,⼀般为c0opcode_2:省略或者为0ldm,stm和栈ldm,stmldr与str只能访问4个字节,当数据较⼤的时候,就会明显的降低效率,这时就需要使⽤到ldm和stm,ldm与stm是⼤量的从寄存器与内存交换数据的⽅式,常⽤于在内存和寄存器之间⼤量读取和写⼊数据:stmia sp {r0 - r12}:stm表⽰进⾏批量数据操作,ia的意思是将r0存⼊SP的内存地址处,然后SP内存地址+4(32位),将r1存⼊该地址,内存地址再+4,存⼊r2,依次存到r12,这就是⼀个寄存器和内存交换⼤量数据的⽰例,在⼀个周期内完成了多个内存地址和多个寄存器的操作。
第8章 基于ARM的Linux内核移植
(2)编译测试参考板的Linux内核 为了测试Linux对参考板的支持情况,最好配置编 译Linux内核,在目标参考板上运行测试一下。 对于交叉开发来说,首先应在顶层Makefile中设置 ARCH、CROSS_COMPILE和EXTRA_VERSION 变量,然后才能选择配置指定的体系结构平台。 ARM平台的例子如下 ARCH := arm CROSS_COMPILE := arm-linuxEXTRA_VERSION :=
8.1.2 开发板内核移植 对于内核移植工作来说,主要是添加开发板初始 化和驱动程序的代码。这部分代码大部分跟体系 结构相关,在arch目录下按照不同的体系结构管理 ,下面以ARM S3C2410平台为例,分析内核代码 移植过程。 Linux2.6内核已经支持S3C2410处理器的多种硬件 板,我们可以参考SMDK2410参考板,来移植开 发板的内核
真正系统平台号的定义位置在arch/arm/tools/mach-types 文件。 #machine_is_xx CONFIG_xx MACH_TYPE_xx number smd2410 ARCH_SMDK2410 SMDK2410 193 arch/arm/tools/mach-types中每一行定义一个系统平台号 。“machine_is_xxx”是用来判断当前的平台号名称 ;CONFIG_xxxx是在内核配置时生成的; MACH_TYPE_xxx是系统平台号的定义;number是系 统平台的值。
#include/asm/mach/arch.h
#define MACHINE_START(_type,_name) const struct machine_desc _mach_desc_##_type \ _attribute_((_section_(“.init”))) = { \ .nr = MACH_TYPE_##_type,\ .name = name, … #define MACHINE_END };
keil arm编译手册
Keil MDK(Microcontroller Development Kit)是一套用于嵌入式系统开发的工具,其中包括编译器、调试器、模拟器等。
以下是关于Keil MDK ARM编译的一些基本步骤。
请注意,具体步骤可能会根据你的项目和硬件平台而有所不同。
Keil MDK ARM 编译手册基本步骤:创建新工程:打开Keil MDK,选择"Project" -> "New µVision Project"。
在弹出的对话框中,选择工程的存储位置和工程名称,然后点击"Save"。
选择目标设备:在弹出的"Device" 对话框中,选择你的目标微控制器或处理器型号,然后点击"OK"。
添加源文件:在"Project" 窗口中,右键点击"Source Group 1",选择"Add New Item to Group 'Source Group 1'",然后添加你的源代码文件。
配置编译选项:在"Project" 窗口中,右键点击你的工程,选择"Options for Target 'Target 1'"。
在弹出的对话框中,配置"C/C++"、"Miscellaneous" 和其他选项,例如选择编译器、调试器、优化选项等。
设置链接脚本(可选):在"Options for Target 'Target 1'" 对话框中的"Linker" 选项卡中,你可以设置链接脚本,以指定程序的内存布局。
编写源代码:打开添加的源代码文件,编写你的程序代码。
构建项目:点击工具栏上的"Build" 按钮或者使用快捷键(通常是F7)进行编译。
arm-linux-gccldobjcopyobjdump使用总结
arm-linux工具的功能如下:arm-linux-addr2line 把程序地址转换为文件名和行号。
在命令行中给它一个地址和一个可执行文件名,它就会使用这个可执行文件的调试信息指出在给出的地址上是哪个文件以及行号。
arm-linux-ar 建立、修改、提取归档文件。
归档文件是包含多个文件内容的一个大文件,其结构保证了可以恢复原始文件内容。
arm-linux-c++flit 连接器使用它来过滤 C++ 和 Java 符号,防止重载函数冲突。
arm-linux-gprof 显示程序调用段的各种数据。
arm-linux-ld 是连接器,它把一些目标和归档文件结合在一起,重定位数据,并连接符号引用。
通常,建立一个新编译程序的最后一步就是调用ld。
arm-linux-nm 列出目标文件中的符号。
arm-linux-objcopy 把一种目标文件中的内容复制到另一种类型的目标文件中。
arm-linux-objdump 显示一个或者更多目标文件的信息。
使用选项来控制其显示的信息,它所显示的信息通常只有编写编译工具的人才感兴趣。
arm-linux-ranlib 产生归档文件索引,并将其保存到这个归档文件中。
在索引中列出了归档文件各成员所定义的可重分配目标文件。
arm-linux-readelf 显示elf格式可执行文件的信息。
arm-linux-size 列出目标文件每一段的大小以及总体的大小。
默认情况下,对于每个目标文件或者一个归档文件中的每个模块只产生一行输出。
arm-linux-string 打印某个文件的可打印字符串,这些字符串最少4个字符长,也可以使用选项-n设置字符串的最小长度。
默认情况下,它只打印目标文件初始化和可加载段中的可打印字符;对于其他类型的文件它打印整个文件的可打印字符。
这个程序对于了解非文本文件的内容很有帮助。
arm-linux-strip 丢弃目标文件中的全部或者特定符号。
arm-linux-gcc -wall -O2 -c -o $@ $<-o 只激活预处理,编译,和汇编,也就是他只把程序做成obj文件-Wall 指定产生全部的警告信息-O2 编译器对程序提供的编译优化选项,在编译的时候使用该选项,可以使生成的执行文件的执行效率提高-c 表示只要求编译器进行编译,而不要进行链接,生成以源文件的文件名命名但把其后缀由 .c 或 .cc 变成 .o 的目标文件-S 只激活预处理和编译,就是指把文件编译成为汇编代码arm-linux-ld 直接指定代码段,数据段,BSS段的起始地址-Tbss ADDRESS Set address of .bss section-Tdata ADDRESS Set address of .data section-Ttext ADDRESS Set address of .text section示例:${CROSS}ld -Ttext=0x33000000 led.o -o led.elf使用连接脚本设置地址:arm-linux-ld -Tbeep.lds start.o beep.o -o beep.elf其中beep.lds 为连接脚本如下:arm-linux-objcopy被用来复制一个目标文件的内容到另一个文件中,可用于不同源文件的之间的格式转换示例:arm-linux-objcopy –o binary –S elf_file bin_file常用的选项:input-file , outflie输入和输出文件,如果没有outfile,则输出文件名为输入文件名2.-l bfdname或—input-target=bfdname用来指明源文件的格式,bfdname是BFD库中描述的标准格式名,如果没指明,则arm-linux-objcopy自己分析3.-O bfdname 输出的格式4.-F bfdname 同时指明源文件,目的文件的格式5.-R sectionname 从输出文件中删除掉所有名为sectionname的段6.-S 不从源文件中复制重定位信息和符号信息到目标文件中7.-g 不从源文件中复制调试符号到目标文件中arm-linux-objdump查看目标文件(.o文件)和库文件(.a文件)信息arm-linux-objdump -D -m arm beep.elf > beep.dis-D 显示文件中所有汇编信息-m machine指定反汇编目标文件时使用的架构,当待反汇编文件本身没有描述架构信息的时候(比如S-records),这个选项很有用。
链接脚本(LinkerScript)用法解析(一)关键字SECTIONS与MEMORY
链接脚本(LinkerScript)⽤法解析(⼀)关键字SECTIONS与MEMORY1.MEMORY关键字⽤于描述⼀个MCU ROM和RAM的内存地址分布(Memory Map),MEMORY中所做的内存描述主要⽤于SECTIONS中LMA和VMA的定义。
2.SECTIONS关键字⽤于定义output section(输出段)的相应input section(输⼊段)、LMA和VMA,是整个连接脚本中最为重要的部分。
注:output section 是实际存储在内存中的“段”,⽽input section是其构成成员,如.data为数据段,由所有全局变量构成(默认情况下);.text为代码段,由所有函数构成(默认情况下)...3.下⾯我们⾸先来介绍MEMORY的语法,MEMORY的语法格式如下:MEMORY{ <name> [(<attr>)] : ORIGIN = <origin>, LENGTH = <len> ...}其中<name>是所要定义的内存区域的名字,<origin>是其起始地址,<len>为内存区域的⼤⼩。
另外,<attr>是可选的,并不重要,具体⽤法可参考GNU Linker 的语法说明。
MEMORY的⼀个具体使⽤实例如下:MEMORY{ rom (rx) : ORIGIN = 0, LENGTH = 256K // MEMORY语法中可以使⽤如K、M和G这样的内存单位 ram (!rx) : org = 0x40000000, l = 4M // ORIGIN可以写为org,⽽LENGTH可以写为l}4.在介绍SECTIONS的⽤法之前,我们先对之前提到的LMA和VMA进⾏说明:每个output section都有⼀个LMA和⼀个VMA,LMA是其存储地址,⽽VMA是其运⾏时地址,例如将全局变量g_Data所在数据段.data的LMA设为0x80000020(属于ROM地址),VMA设为0xD0004000(属于RAM地址),那么g_Data的值将存储在ROM中的0x80000020处,⽽程序运⾏时,⽤到g_Data的程序会到RAM中的0xD0004000处寻找它。
winarm使用
狗拿耗子initialize———狗拿耗子第二篇 gcc 编译时如果带上“-v” ,则可以看到 gcc 给你的程序链接上的藏在犄角旮旯里 面的.o 文件与库文件。
正如你所猜想的一样, 会在你的程序前后添上一些代码, gcc 完成一些不为人知的功能。
本文接下来的叙述将会为你揭开这一切的“秘密” 。
1、collect2.exe 与 gcc 提供的 5 个.o 文件 首先写一个什么都不干的 test.c,里面提供了必不可少的系统调用的函数_exit() 与_sbrk_r(),以及一个空的 main()。
执行 arm-efl-gcc test.c –v –o null.elf,shell 会出 现一堆打印,如下只贴出来最重要的部分。
很容易看到首先 gcc 用 as 将源代码编译成一个临时文件 ccwTbaaa.o。
然后 gcc 用 collect2.exe 将 crti.o、crtbegin.o、crt0.o、ccwTbaa.o、crtend.o、crtn.o 处理成目标文 件 null.elf。
编译用的编译器是 winarm 提供的,winarm 是在 windows 下 arm 处理器的 gcc 开发环境。
可以从 http://www.siwawi.arubi.uni-kl.de/avr_projects/arm_projects/下载到 最新的版本,下载完成后无需编译,直接使用即可。
winarm 没有使用常用的 c 库 glibc , 而 是 采 用 了 redhat 专 门 对 嵌 入 式 系 统 开 发 的 c 库 newlib , 可 以 从 /newlib/下载到 newlib 最新的源代码。
winarm 的自叙文件中没 有提到 c++库,不过从 STL 库文件中可以推测出 winarm 用的就是 gcc 提供的 c++ 库,即 libstdc++ v3,gcc 的下载地址为 /。
arm汇编语言编程实验报告
arm汇编语言编程实验报告实验二ARM汇编语言编程实验.doc班级计算机科学与技术(嵌入式)学号课程名称ARM体系结构姓名实验成绩日期指导教师冯灵霞实验报告院系信息工程学院一、实验目的1、掌握ADT IDEARM开发环境中基本的工程设置以及程序编译方法。
2、掌握ADT IDEARM开发环境中基本的程序调试方法。
3、掌握基本的ARM汇编语言编程方法。
二、实验内容用汇编语言编写一个程序实现如下目的:从源地址拷贝num个字(num*4个字节)的数据到目的地址dst中。
三、预备知识1、ARM汇编语言的基础知识。
2、程序调试的基础知识和方法。
四、实验设备1、硬件:JXARM9-2440教学实验箱、PC机。
2、软件:PC机操作系统Windows 98(2000、XP)+ADT IDE开发环境。
五、基础知识ADT IDE集成了GNU汇编器arm-elf-as、编译器arm-elf-gcc和链接器arm-elf-ld。
在ADT IDE中编写的程序必须符合GNU的语法规则。
下面介绍一些基本的GNU汇编知识以及本实验用到的ARM 汇编指令。
1、GUN汇编语言语法及规则1)_start_start为程序默认入口点,代码段默认起始地址为0x800,如果需要修改可以在链接脚本文件中指定。
2)标号语法:symbol:symbol为定义的符号。
说明:上述代码定义一个标号,它表示程序中当前的指令或数据地址。
如果在程序中出现两个相同的标号,汇编器将会产生一个警告,且只有第一个标号有效。
2、GNU汇编语言伪操作1).equ伪操作语法:.equ symbol,exprexpr为基于寄存器的地址值、程序中的标号、32位的地址常量或位的常量。
symbol为.equ伪操作为expr定义的字符名称。
说明:该操作符为数字常量、基于寄存器的值和程序中的标号定义一个字符名称,相当于C语言中的宏定义。
示例:.equ USERMODE,0x102).global伪操作符语法:.global symbolsymbol为声明的符号的名称。
ARMlinux启动分析-Nathan.Yu的专栏-CSDN博客
ARMlinux启动分析-Nathan.Yu的专栏-CSDN博客linux启动分析(1)---bootloader启动内核过程我分析的是2.4.19的内核版本,是xscale的平台,参考了网上很多有价值的帖子,也加入了自己的一些看法,陆续总结成文字,今天是第一篇:内核一般是由bootloader来引导的,通过bootloader启动内核一般要传递三个参数,第一个参数放在寄存器0中,一般都为0,r0 = 0;第二个参数放在寄存器1中,是机器类型id,r1 = Machine Type Number;第三个参数放在寄存器2中,是启动参数标记列表在ram中的起始基地址;bootloader首先要将ramdisk(如果有)和内核拷贝到ram当中,然后可以通过c语言的模式启动内核:void (*startkernel)(int zero, int arch, unsigned int params_addr) = (void(*)(int, int, unsigned int))KERNEL_RAM_BASE;startkernel(0, ARCH_NUMBER, (unsigned int)kernel_params_start);其中KERNEL_RAM_BASE为内核在ram中启动的地址,ARCH_NUMBER是Machine Type Number,kernel_params_start 是参数在ram的偏移地址。
这时候就将全力交给了内核。
linux启动分析(2)---内核启动地址的确定内核编译链接过程是依靠vmlinux.lds文件,以arm为例vmlinux.lds文件位于kernel/arch/arm/vmlinux.lds,但是该文件是由vmlinux-armv.lds.in生成的,根据编译选项的不同源文件还可以是vmlinux-armo.lds.in,vmlinux-armv-xip.lds.in。
链接脚本文件语法详细讲解
我们对每个c或者汇编文件进行单独编译,但是不去连接,生成很多.o 的文件,这些.o文件首先是分散的,我们首先要考虑的如何组合起来;其次,这些.o文件存在相互调用的关系;再者,我们最后生成的bin文件是要在硬件中运行的,每一部分放在什么地址都要有仔细的说明。
我觉得在写makefile的时候,最为重要的就是ld的理解,下面说说我的经验:首先,要确定我们的程序用没有用到标准的c库,或者一些系统的库文件,这些一般是在操作系统之上开发要注意的问题,这里并不多说,熟悉在Linux编程的人,基本上都会用ld命令;这里,我们从头开始,直接进行汇编语言的连接。
我们写一个汇编程序,控制GPIO,从而控制外接的LED,代码如下;.text.global _start_start:LDR R0,=0x56000010 GPBCON寄存器MOV R1,# 0x00000400str R1,[R0]LDR R0,=0x56000014MOV R1,#0x00000000STR R1,[R0]MAIN_LOOP:B MAIN_LOOP代码很简单,就是一个对io口进行设置然后写数据。
我们看它是如何编译的,注意我们这里使用的不是arm-linux-gcc而是arm-elf-gcc,二者之间没有什么比较大的区别,arm-linux-gcc 可能包含更多的库文件,在命令行的编译上面是没有区别。
我们来看是如何编译的:arm-elf-gcc -g -c -o led_On.o led_On.s 首先纯编译不连接arm-elf-ld -Ttext 0x00000000 -g led_On.o -o led_on_elf用Ttext指明我们程序存储的地方,这里生成的是elf文件,还不是我们真正的bin,但是可以借助一些工具可以进行调试。
然后:arm-elf-objcopy -O binary -S led_on_elf led_on.bin生成bin文件。
arm重定向c库函数
arm重定向c库函数ARM是一种广泛应用于嵌入式系统和移动设备的处理器架构。
在ARM 架构上,重定向C库函数是一种常见的技术,可以提供自定义的库函数实现,以替代默认的C库函数。
本文将介绍ARM重定向C库函数的原理和使用方法。
一、ARM架构和C库函数ARM(Advanced RISC Machines)架构是一种基于精简指令集(RISC)的处理器架构,由ARM公司开发。
ARM架构广泛应用于各种嵌入式系统和移动设备,如智能手机、平板电脑、物联网设备等。
在ARM 架构上,C语言是一种常用的编程语言,用于开发嵌入式应用程序。
C库函数是一组预定义的函数集合,用于提供常用的功能和算法实现。
例如,stdio.h头文件中包含了输入输出相关的函数,如printf、scanf等;string.h头文件中包含了字符串处理函数,如strcpy、strlen等。
这些C库函数通常由编译器提供,并与操作系统紧密集成。
二、ARM重定向C库函数的原理在ARM架构中,重定向C库函数是一种通过修改链接脚本或使用编译器选项的技术。
它允许开发者提供自定义的库函数实现,以替代默认的C库函数。
重定向C库函数的原理如下:1. 编写自定义库函数:开发者需要编写自定义的库函数,实现与默认C库函数相同的功能。
例如,如果要重定向printf函数,则需要编写一个名为printf的函数,实现与默认的printf函数相同的功能。
2. 修改链接脚本或使用编译器选项:开发者需要修改链接脚本或使用编译器选项,指示编译器使用自定义的库函数。
这样,在链接时,编译器会将默认的C库函数替换为自定义的库函数。
3. 运行时重定向:在程序运行时,当调用C库函数时,链接器会根据链接脚本或编译器选项,将函数调用重定向到自定义的库函数。
这样,程序将使用开发者提供的自定义库函数实现。
三、ARM重定向C库函数的使用方法ARM重定向C库函数的使用方法因编译器而异。
以下是一些常用的编译器和使用方法示例:1. GCC编译器:GCC是一种常用的开源编译器,支持ARM架构。
ARM_gcc_linker_script
ARM GCC linker 脚本介绍Team MCUZONE整理自网络文章在输入文件在进行链接的时,每个链接都由链接脚本控制着,脚本由链接器命令语言组成。
脚本的主要目的是描述如何把输入文件中的节(sections)映射到输出文件中,并控制输出文件的存储布局。
大多数的链接脚本就是做这些事情的,但在有必要时,脚本也可以指导链接器执行一些其他的操作。
链接器总是使用链接器脚本,如果你没有提供一个自定义的脚本文件的话,编译器会使用一个缺省的脚本。
1链接器脚本的基本概念链接器把一些输入文件联合在一起,生成输出文件。
输出的文件和输入文件都是特定的object文件格式,每个文件都可被称为对象文件(object file),而且,输出文件还经常被称为可执行文件。
但这里我们依然称之为对象文件。
每个对象文件在其中都包含有一个段(section)列表,我们有时称输入文件中的段(section)为输入段(input section),同样,输出文件中的节称为输出段(output section)。
对象文件中的每一个段都有名字和大小。
大多数的段还有一个相连的数据块,就是有名的"section contents"。
一个被标记为可加载(loadable)的段,意味着在输出文件运行时,contents 可以被加载到内存中。
没有contents的节也可以被加载,实际上除了一个数组被设置外,没有其他的东西被加载(在一些情况下,存储器必须被清0)。
而既不是可加载的又不是可分配的(allocatable)段,通常包含了某些调试信息。
每个可加载或可分配的输出段(output section)都有2个地址。
第一个是虚拟存储地址VMA (virtual memory address),这是在输出文件执行时该段所使用的地址。
第二个是加载存储地址LMA(load memory address),这是该段被加载时的地址。
在大多数情况下,这两个地址是相同的。
通过简单示例学习链接脚本基本语法
.text : { *(.text) }>.sram
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] { OUTPUT-SECTION-COMMAND OUTPUT-SECTION-COMMAND … } [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]
通过简单示例讲解连接脚本基本语法 王兴伟
Linux博客地址: SOC FPGA客户在采用bm模式或简单实时操作系统时,有时会需要修改工程代 码链接的起始地址和地址空间长度。或者在链接脚本中预留栈空间和定义全局变 量等。本文以骏龙公司提供的baremetal 模式代码工程的脚本为例,介绍一下链 接脚本的基本知识,希望起到抛砖引玉的作用。
.text . : { *(.text) } 和 .text : { *(.text) } 这两个描述是截然不同的,第一个将.text section 的VMA 设置为定位符号的 值,而第二个则是设置成定位符号的修调值,满足对齐要求后的。 ADDRESS 可以是一个任意表达式,比如ALIGN(0×10)这将把该section 的 VMA 设置成定位符号的修调值,满足16 字节对齐后的。 注意:设置 ADDRESS 值,将更改定位符号的值。 另外,可以直接修改的值,如 . = 0×10000;
ARM Cortex—M0+机器码文件分析方法
ARM Cortex—M0+机器码文件分析方法作者:蔡伯峰蒋建武王宜怀来源:《现代电子技术》2017年第14期摘要: MCU深层次应用开发需要开发人员深入了解机器码在MCU中的存储和执行机制,但机器码自身可读性差、相互关系不清晰等特点决定了对其阅读、查找、分析难度较大。
针对这一情况,以采用ARM Cortex⁃M0+内核的KL25 MCU为蓝本,根据工程编译链接过程和链接脚本文件(.ld),分析机器码文件的生成机制和组织结构。
在此基础上针对机器码文件中的中断向量表、初始化代码、函数、常变量、FLASH配置域等主要内容,结合机器码系列文件,给出了简明快捷实用的分析方法,为嵌入式开发人员优化及动态更新程序和数据、设计机器码下载软件等提供支撑,对其他内核机器码文件的分析有借鉴意义。
关键词: ARM Cortex⁃M0+;机器码文件; KL25;链接脚本中图分类号: TN918.2⁃34; TP311 文献标识码: A 文章编号: 1004⁃373X(2017)14⁃0044⁃05Abstract: MCU deep⁃level application development requires its developers to understand storage and enforcement mechanisms of machine codes in MCU, but the machine code itself has poor readability and unclear structure, thus it is difficult to be directly read, searched and analyzed. In response to this situation, using the ARM Cortex⁃M0+ KL25 MCU as a model, and according to engineering compiling linking process and linker script files (.ld) of MCU application project,the generative mechanism and structure of machine code file are analyzed. On this basis, a concise, efficient and practical analysis method is given in allusion to the interrupt vector table,initialization code, functions, constants, variables and Flash configuration domain. This is helpful for the embedded developers to update dynamically and optimize programs and data, design machine code download software, and analyze machine code files of other kernels.Keywords: ARM Cortex⁃M0+; machine⁃code file; KL25; linker script0 引言用C语言开发的MCU应用工程经过编译链接后生成机器码目标文件,正确全面深入地掌握机器码文件的分析方法是快速进行MCU深层次应用开发必备的技能。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
这个链接脚本表示代码段从0x30000000开始加载,然后后面的.rodata,.data,.bss段都分别加在其后,并且后面的每个段的起始地址是按照4个字节对齐的。
(2):
ENTRY(_start);
OUTarm", "elf32-bigarm", "elf32-littlearm");
a*.o(.text)
}
.rodata : {
*(.rodata);
}
.data : {
*(.data);
}
链接脚本:
=================
什么是链接脚本,就是用于告诉链接器如何把输入文件内的各个段(section)放到输出文件中,并控制输出文件中的各个段在此程序运行时的地址空间布局。一个程序由多个段组成,那么这些段是如何在文件中存放的,以及是如何加载到内存的相应位置进行执行的呢,这个就是通过连接脚本进行控制的。
. = ALIGN(4);
.data : {
*(.data);
}
. = ALIGN(4);
.bss : {
*(.bss);
}
AT, 后面跟LMA, 这个是表示当我们把目标文件拷贝成二进制的时候,该段在文件中物理存放位置的偏移。这个可以用来把多个不同的部分的代码写到一个文件中,然后烧写到flash上去,然后,程序在运行的时候再把它从AT指定的位置读到内存的另外一个位置上去。
contents,内容里面指定把哪些文件里面的哪些段或者该文件全部输出到secname所指定的这个段中。比如*(.text)就表示所有输入文件的.text段。括号外面表示文件名称,括号里面表示这些文件里面的什么段。
. = ALIGN(4);
.text : {
*(.text);
}
. = ALIGN(4);
.rodata : {
*(.rodata);
}
_bss_start = .;
.bss : {
*(.bss);
}
_bss_end = .;
}
其实secname在copy成binary文件的时候已经没有了,这个只是在有操作系统的情况下才有用。因此在拷贝成二进制的时候,前面的段名称根本不是很重要,只是后面的的地址和在文件中的地址才是最关心的。
如果我们在SECTIONS内部定义了变量,那么这个相当于在这个位置定义了一个变量,如果我们要取这个变量地址,我们需要在C语言中使用&_bss_start来访问。
}
常见的例子:
(1):
ENTRY(_start);
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm");
OUTPUT_ARCH(arm);
SECTIONS
{
. = 0x50008000;
OUT_ARCH(arch);设置输出文件的体系架构。
SECTIONS命令:最重要的,最基本的,也是最主要的命令,它告诉链接器如何把输入文件的各个section输出到目标文件中的各个section中去。
SECTIONS命令的格式如下:
SECTIONS
{
一条或者多条section-command
{
first 0x0 : {
led.o
}
.text 0x30000000 : AT(4096) {
*(.text);
}
.text 0x32000000 : AT(5100) {
链接脚本格式:
链接脚本由一系列命令组成,每一个命令由一个关键字和相应的参数,或者一些赋值语句等组成。命令由分号进行分割。用/* */进行注释。
常见命令:
ENTRY(SYMBOL);将SYMBOL的值设置成入口地址。一般设置为_start。
OUTPUT(FILENAME); 定义输出文件的名字。可以用它来指定默认的输出文件名称。当然我们一般都用手动-o进行指定,如果我们没有进行手动指定的话,输出文件名称就以这个FILENAME为输出文件名。
STARTUP(filename);指定filename为第一个输入文件。
OUTPUT_FORMAT(default, big, little);定义3种输出文件的格式。若有命令行选项-EB(大端),则使用第二个输出格式,有命令行指定-EL(小端),则使用第三个格式。否则使用默认的default输出格式。
或者符号赋值语句
}
section-command的常见格式如下:
secname [address] : [AT(LMA)]
{ contents }
首先中括号的选项是可选的,可以不写。
secname, 指定输出的段名称。
address, 表示程序的VMA地址。也就是表示当执行此程序的时候程序加载器应该把这个段加载到内存的哪个地址。如果没有指定这个地址,链接器根据定位符号‘.‘的值设置该section的VMA。
例子:
SECTIONS {
. = 0x30000000; //表示设置当前符号的值为0x3000000
.text : { *(.text) } //表示把所有输入文件的代码段集合在一起,起始运行地址就为当前定位符号的值,-- 0x30000000
.rodata ALIGN(4) : { *(.rodata) } // 在输出文件中它紧挨着.text段存放。
OUTPUT_ARCH(arm);
SECTIONS
{
start 0x00000000 : { start.o }
main 0x30000000 : AT(4096) { main.o hello.o }
}
上面表示把start.o的运行地址指定为0x000000, 然后main.o hello.o程序的运行地址指定为0x30000000,当我们把链接后生成的可执行文件通过objcopy出来之后,那么start.o的二进制代码就从文件的0偏移开始存放,main.o hello.o就从同一个文件的4096这个位置开始存放。当时main.o hello.o是挨着存放的,并没有把他们的相同段放在同一个段里面。
======================================
ENTRY(_start);
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm");
OUTPUT_ARCH(arm);
SECTIONS