编译过程并不神奇

发布时间:2019-08-06 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了编译过程并不神奇脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

编译过程并不神奇

工具

过程的简单描述

这篇文章尽可能地说清楚从编译程序到把代码烧到板子里,然后开始后运行程序。很多细节是根据Arduino来写的,因为Arduino没有外接的serial flash或者外接的SDRam,所以相对来说简单一些。但总体来说这个过程对于嵌入式设备都是十分相似的。

当按下编译按钮后,编译器compiler将每个.c文件编译成汇编语言,汇编语言是机器可以识别的语言。拿项目举例来说,LED.c经过编译器生成LED.o。然后连接器linker把每个汇编文件(*.o)连接在一起,生成最终的编译文件并通过avrdude或者PRogrammer下载到arduino的Flash中开始运行。

编译过程

c 语言翻译成汇编语言

//c language
static void Task4(void)
{
  USART_TransmIT("Task4rn", strlen("Task4rn"));
}

翻译成汇编语言是

ldi    r22, 0x07    ; 7    //"Task4rn" 字符串长度7的low byte 存入寄存器22
ldi    r23, 0x00    ; 0    //"Task4rn" 字符串长度7的high byte 存入寄存器23
ldi    r24, 0x1E    ; 30   //"Task4rn" 的内存位置0x011E的low byte存入寄存器24,这里强调一下因为计算机架构的关系,这个位置在map文件中被翻译成0080011E,后面会在详细说明。
ldi    r25, 0x01    ; 1    //"Task4rn" 的内存位置0x011E的high byte存入寄存器24 
call    0x1f4    ; 0x1f4 <USART_Transmit>  //把当前的地址压到stack里,然后去地址0x1f4调用函数USART_Transmit,这个函数会利用r22,r23,r24和r25
ret                        //从stack里面弹出返回地址,去这个返回地址开始运行接下来的命令

大家可以参考Atmel官方对ret的解释,可以清楚的看到,ret命令把stack的东西弹出然后放入PC(Program Counter:里面记录着当前运行的代码的位置)里面。

内存空间分成几个section

经过编译器的编译后,c文件中的代码,和变量等会被存放到不同的区域section,参考GCC Sections
这里面比较重要的是text section, data section和bss section:

  • text section存放代码
  • data section存放初始化过的变量和常量。初始化的时候,会从Flash里把初始化值拷贝到Ram里。请参考后文,有详细的初始化汇编语言。
  • bss section存放未经过初始化的变量。初始化的时候,整个区域都会被初始化为0。请参考后文,有详细的初始化汇编语言。

举个例子来说明,对于下面的c代码:

uint16_t Data = 0x1234;
uint32_t Result;

uint32_t Power(void)
{
  Result = Data * Data;
  USART_Transmit("done!", 5);
  return Result;
}

经过编译后我们可以得到:

编译过程并不神奇

  • symbol table里面记录了text 和data section的起始位置,以及每个函数和变量相对于对应section的偏移。

    • 里面的函数USART_Transmit 因为并没有在这个文件中定义,所以编译器并不知道它在哪里,后面连接器linker会到其他文件中找到这个函数的定义然后把它补上。
  • text section存着函数Power()的汇编语言,里面会去地址0x00F0取得Data的值,还会把运算的结果保存到地址0x00F8的Result里面。
  • data section存着Data的数值0x1234,还有ASCII字符串"done!"。在c语言里字符串都是用0x0结尾的,0x0也会占用一个byte的空间,这里字符串的名字(String_1)是编译器自己命名的,也可能是其他的名字。
  • bss section存着Result的数值,bss区域的数据都会初始化0。

Atmel328P的Flash和Ram

Arduino所使用的MCU是Atmel328P,根据数据手册,Atmel328P有2KB的SRAM和32KB的Flash,以及1KB的EEProm。细节请参考Atmel328P的section 12.2,我截取了数据手册中的一幅图:

编译过程并不神奇

  • 比如我们在Arduino经常使用的一些GPIO的寄存器,PORTB, DDRB都是存在IO registers中的。我们可以找到详细的register summary在数据手册section 35。
  • 根据之前对编译器section的讲解,最后text section和data section都会下载到Arduino的Flash中,因为text section中的代码在运行的时候并不会被改变,data section中的初始值在板子初始化的时候会从Flash中拷贝到SRAM中,bss section会在板子初始化的时候被初始化为0。
  • 细看2KB的Internal SRAM:

    • 首先放的是Data section然后是BSS section,剩下的部分都是给Heap(堆)和Stack(栈)。简单地说当我们使用Malloc拿到的内存都是从heap里面取得的,而函数的参数,返回值以及在函数内声明的local VARiable都是向栈里面push进去和pop出来的。
    • 在嵌入式开发中,因为内存资有限,经常地malloc/free堆的内存空间,会减低内存的利用率,所以一般情况我们不经常使用malloc去拿堆里面的空间。

编译过程并不神奇

连接过程

当编译器把所有的.c文件都编译好后,连接器linker就会过来把所有的.c文件集合起来生成一个总的文件。在Atmel中,最后生成的是.elf文件。连接器的作用是把所有的undefined variable都找到,把它们的地址都补全,然后把所用的相对位置都计算出绝对位置,比如前面例子中,symbol table里的函数USART_Transmit是未知位置的,这是linker就会去其他编译文件中找这个函数的定义,并得到地址。最后生成的总的文件类似于第一张图的样子,也是开始是symbol table然后是text section,data section和bss section。

Atmel328P的编译文件的连接

  • 连接器linker需要它的配置文件,被称为linker file。对于Atmel328P来说,这个文件叫avr51.x,它在Atmel Studio的安装文件夹里,我的路径是(C:Program Files (x86)AtmelStudio7.0toolchainavr8avr8-gnu-toolchainavrlibldscriptsavr51.x)。打开avr51.x。

    • 首先是定义了每个section的位置,里面的data ORIGIN前两个数字0x80,eeprom ORIGIN前两个数字0x81是和总线相关,并不意味着SRAM的地址真的从0x800100开始。
    • 然后也定义了在main函数之前,Flash中应该哪些代码,包括中断向量,初始化stack pointer,heap的地址,初始化data section和bss section等等
MEMORY
{
  text   (rx)   : ORIGIN = 0, LENGTH = __TEXT_REGION_LENGTH__
  data   (rw!x) : ORIGIN = 0x800100, LENGTH = __DATA_REGION_LENGTH__
  eeprom (rw!x) : ORIGIN = 0x810000, LENGTH = __EEPROM_REGION_LENGTH__
  fuse      (rw!x) : ORIGIN = 0x820000, LENGTH = __FUSE_REGION_LENGTH__
  lock      (rw!x) : ORIGIN = 0x830000, LENGTH = __LOCK_REGION_LENGTH__
  signature (rw!x) : ORIGIN = 0x840000, LENGTH = __SIGNATURE_REGION_LENGTH__
  user_signatures (rw!x) : ORIGIN = 0x850000, LENGTH = __USER_SIGNATURE_REGION_LENGTH__  
}
.text:
{
  *(.vectors)
    KEEP(*(.vectors))
    *(.init0)  /* Start here after reset.  */
    KEEP (*(.init0))
    *(.init1)
    KEEP (*(.init1))
    *(.init2)  /* Clear __zero_reg__, set up stack pointer.  */
    KEEP (*(.init2))
    *(.init3)
    KEEP (*(.init3))
    *(.init4)  /* Initialize data and BSS.  */
    KEEP (*(.init4))
    *(.init5)
    KEEP (*(.init5))
    ...
}

Atmel328P的Map和Lss文件

当Atmel328P编译完成后,除了会生成最后的编译文件.elf外,还会生成.map和.lss文件,很多时候这两个文件可以帮我们很好的debug程序,理解程序。

  • .lss文件包括了整个项目的汇编代码,text section的每条代码。
  • .map就是一个symbol table,里面包含了所有symbol的位置。

下面我们来简单分析一下两个文件:

IoT_Ethernet.lss

在IoT_Ethernet.lss,我挑了几段我认为比较有意思的地方和大家分享下:

1. data, text, bss section的地址:

Idx Name          Size      VMA       LMA       File off  Algn
  0 .data         00000064  00800100  00000676  0000070a  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  1 .text         00000676  00000000  00000000  00000094  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .bss          00000096  00800164  00800164  0000076e  2**0
                  ALLOC
  • VMA vs LMA:上表中记录了每个section的大小和地址,值得注意的是这里有两个地址,一个是VMA(Virtual Memory Address),另外一个是LMA(Load Memory Address)。根据这个解释,VMA是当板子经历过初始化阶段(startup)后,该section的地址;LMA是这个section的数据从哪里加载。所以:

    • data section的大小是100bytes (十六进制0x64,十进制100),从地址0x800100开始,当初始化阶段,会从地址0x676把data section拷贝到地址0x800100(这个是RAM地址)。这里0x676是data section初始化数据在Flash中的地址,也正好是从text section结束的地方。
    • text section的大小是676bytes,从地址0x0开始,它的Load Memory Address 也是0x0。意味着不需要在初始化时候进行拷贝。
    • bss section的大小是96bytes,从地址0x800164开始,它的Load Memory Address 也是0x0。

2. 初始化data 和bss section的汇编代码:

这段程序是在初始化data section。在这段assSEMbly code中,程序从Flash的地址0x676中复制0x64 bytes(十六进制0x64,十进制100)的数据到地址0x00800100。这里注意两条汇编,一个是LPM,一个是ST,从Atmel Assembly的官方网站,LPM: Load Program Memory,是从program memory拿出数据,这里的program memory指的是Flash。另外一个是ST: Store Indirect From Register to Data space using Index X,这里的data section指的是SDRAM,SDRAM从0x00800000开始,所以仔细分析这些汇编会发现,这里的st X+, r0,X寄存器(r26和r27合在一起)的范围是从0X100开始,但实际是把数据存到了0x00800100。

00000074 <__do_copy_data>:
  74:    11 e0           ldi    r17, 0x01    ; 1
  76:    a0 e0           ldi    r26, 0x00    ; 0
  78:    b1 e0           ldi    r27, 0x01    ; 1
  7a:    e6 e7           ldi    r30, 0x76    ; 118
  7c:    f6 e0           ldi    r31, 0x06    ; 6
  7e:    02 c0           rjmp    .+4          ; 0x84 <__do_copy_data+0x10>
  80:    05 90           lpm    r0, Z+
  82:    0d 92           st    X+, r0
  84:    a4 36           cpi    r26, 0x64    ; 100
  86:    b1 07           cpc    r27, r17
  88:    d9 f7           brne    .-10         ; 0x80 <__do_copy_data+0xc>

3. 中断向量:

当中断发生时,程序会跟据具体是哪个中断向量(比如定时器中断,外部中断等)来这个中断向量表中找到中断ISR(Interrupt Service Routine)的地址。比如我目前的这个中断向量表中,当Timer0的compare match interrupt发生的时候,程序会到0x59C执行ISR;当UART接收到数据的时候,程序会到0x222执行ISR。如下面的代码是中断向量表的一部分:

00000000 <__vectors>:
   0:    0c 94 34 00     jmp    0x68    ; 0x68 <__ctors_end>
   4:    0c 94 51 00     jmp    0xa2    ; 0xa2 <__bad_interrupt>
   8:    0c 94 51 00     jmp    0xa2    ; 0xa2 <__bad_interrupt>
...
  38:    0c 94 ce 02     jmp    0x59c    ; 0x59c <__vector_14>
...
  48:    0c 94 11 01     jmp    0x222    ; 0x222 <__vector_18>
...

IoT_Ethernet.map文件

这个文件可以理解为是一个symbol table,里面包括了项目所有symbol的信息。比如

 .text.LED_GetStatus
                0x00000428       0x20 LowLevel/LED.o
                0x00000428                LED_GetStatus
  • 函数LED_GetStatus是在Flash地址0x428,assembly code一共占32 Byte。
  • 同样在data和bss section也包含了很多信息。

Motorola Hex Format(.hex)

因为我们使用AvrDude通过usb下载代码到板子上,而AvrDude只接收Intel Hex Format,生成能被Arduino Bootloader识别的数据。使用WinAVR可以将.elf文件传变HEX文件(Atmel Studio已经帮我们做这一步了)。简单地说Intel Hex Format每行在说往某个特定的地址写特定的数据。Intel Hex Format参考链接
提取出.hex文件中比较直观的一行来说:

:10066800FFFF4765744C6564537461747573005378
  • 这里是说把0xFF, 0xFF, "GetLedstatus", 0x53, 0x78拷贝到地址0x668。
  • AvrDude根据hex文件生成Arduino bootloader可以识别的数据通过USB接口发送给Arduino。后面bootloader的部分会讲发送的数据。

Arduino Bootloader

Bootloader是在Flash里面的一段代码,用来把新的代码(新的代码从AvrDude发到Arduino的16U2芯片,然后16U2芯片通过uart发送到Atmel328P)通过USB写到Flash的0x00地址。如果没有BootLoader,我们只能通过programmer来烧代码到Arduino,下图是Avr ISP MKII,把它插到Arduino的ICSP header上,就可以在没有bootloader的情况下给Arduino下载代码了。如果是新购买的Arduino,它里面已经有Bootloader了,它使用的是optiboot,它是open source的,这里是它的github repository。下面我们更详细的说下bootloader。

编译过程并不神奇

FUSE 设置

在之前的linker file里面有Fuse的地址:fuse (rw!x) : ORIGIN = 0x820000, LENGTH = __FUSE_REGION_LENGTH__。Fuse里面的信息是配置芯片的关键信息,查看和修改它的信息需要用ISP,一般USB接口没办法访问Fuse。

  • Atmel328P data sheet的第30章,Boot Loader Support详细说明了Fuse的信息。这里我附里面的两张图:可以看到,在Flash的底端是bootloader区域,我们正常的代码每次是下载到从0x0000开始的区域。配置Fuse寄存器,我们可以告诉芯片目前芯片里是否存了bootloader然后bootloader的大小是多少,bootloader的起始地址是什么。当芯片开始上电的时候,芯片实际上是从bootloader的起始位置开始运行代码,它会在bootloader里面停很短的时间,看是否有新的代码要下载到0x0000地址,如果没有就去0x0000开始执行那里的代码,如果有就下载新的代码到0x0000地址。

    编译过程并不神奇


    编译过程并不神奇

Optiboot

当AvrDude拿到Intel Hex Format(.hex)的代码,它会转变成Atmel STK500格式的代码,因为optiboot可以识别Atmel STK500 格式。当AvrDude要发送新的代码给Atmel328P,这是Atmel328P会先reset,然后AvrDude会发送STK500格式的数据给optiboot,optiboot会处理这些数据然后把相应的代码从Flash的0x0000地址开始写入。
当代码下载完成,就重新reset芯片,并且等待程序跳出optiboot,就可以从0x0000开始执行新的代码了!

脚本宝典总结

以上是脚本宝典为你收集整理的编译过程并不神奇全部内容,希望文章能够帮你解决编译过程并不神奇所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。