AVR-GCC生成代码
这一篇文章将通过一些列不同的的示例代码来探讨AVR-GCC生成的代码。
AVR-GCC, C语言, 反编译, 编译器, ABI, AVR, ISA, 程序二进制接口, 指令集架构, 栈, 内存结构, 静态变量, 局部变量, 优化, 目标文件, 嵌入式编程
--by Captdam @ Nov 9, 2025Index
最基础 - 通常意义上的AVR
我们先来看一个简单的C语言代码:
avr-hello.c
volatile char a = 'A', b, c = 'C', d;
void main() {
volatile char z = 'Z';
}
在这个例子中,我们创建了4个全局变量a、b、c、d,并对其中2个赋值。在主函数中,我们定义了一个局部变量z。
我们使用volatile关键字告诉编译器不要优化这些变量。我们将在之后篇幅中详细讨论。
avr-gcc -O0 avr-hello.c -o avr-hello.o
avr-objdump -m avr2 -Dxs avr-hello.o
将源代码avr-hello.c编译为目标文件avr-hello.o。在这里,我们使用-O0参数以禁用编译优化,强制编译器将C源代码逐行编译为机器码,以方便我们研究编译结果。不然的话,编译器将会生成大量非常反直觉,但是可以提高效率的代码。
通常来说,我们应该使用-mmcu参数定义架构(单片机型号)。这便于编译器使用正确的IO寄存器地址、内存大小、支持指令集(ISA)。如果忽略的话,编译器将使用默认架构avr2。
反编译目标文件avr-hello.o中的所有章节。反编译器将需要我们定义架构。在这里,我们使用-m avr2参数告诉反编译器我们使用的ISA,以便反编译器正确解码机器码。
我们使用-Dxs3个参数获得我们需要的所有内容:
-D- 反编译所有章节(section)。这将会翻译机器码到(方便人类阅读的)汇编助记符,便于阅读程序章节(text segment)。-x- 显示所有头文件内容,包括章节列表与符号表,便于查找目标文件中的函数与变量。-s- 显示所有内容。这将显示所有章节的二进制编码与对应的ASCII字符,方便阅读数据章节(data segment)。
下面展示了目标文件的内容,让我们逐步分析:
RAM中的数据 - 静态(全局)变量与堆(局部)变量
volatile char a = 'A', b, c = 'C', d;
SYMBOL TABLE:
00800060 l d .data 00000000 .data
00800062 l d .bss 00000000 .bss
00800062 g O .bss 00000001 b
00800061 g O .data 00000001 c
00800063 g O .bss 00000001 d
00800060 g O .data 00000001 a
上面展示了符号表中的部分内容。静态(全局)变量分为2段:.data段为初始化的全局变量(我们在声明变量时就提供了初始值),.bss段为不需要初始化的全局变量。
不要和C语言中的静态static变量混淆了。本文中“静态”static特指静态分配内存地址的变量。这个地址是在编译时就确定并不会被改变的。相反,C语言中,全局静态变量指变量生命周期为文件层面,局部(函数内)静态变量指变量只定义一次。
第一个是RAM最开头的.data段。在这个例子(使用avr2家族)中,内存地址的最前面96个地址为32个通用寄存器(GPR R0至R31)和64个IO寄存器(注意,部分地址并未被使用)。因此,数据内存能使用的第一个地址为96,也就是十六进制的0x60。
紧接着.data段就是.bss段。
在这个例子中,我们有2个初始化的静态变量a、c,都是char类型,各自使用1字节空间。因此,.data段占用2字节空间,从0x60(包括)到0x62(不包括)。接下来是2个未初始化的静态变量b、d,都是char类型,各自使用1字节空间。因此,.bss段占用2字节空间,从0x62(包括)到0x64(不包括)。
虽然我们声明b早于c,符号表中c的地址却先于b的地址。AVR-GCC修改了变量的地址以便于将同一个段的变量放在一起。
静态(全局)变量与堆(局部)变量
对于C语言来说,只有全局变量与函数内的static变量会被分配到.data或.bss段。它们的生命周期涵盖整个程序,因此,它们可以在程序初始化时就被分配内存空间,且永远不会被收回空间。换言之,这些内存空间将被永远占用。因此,在编译时我们就可以分配一个数据内存空间给它们。
相反,函数内的局部变量不能被分配到.data或.bss段。它们只在进入函数时被创建,并将在函数返回时被销毁。它们将会被创建在堆中。当一个函数被调用时,这个函数首先就会通过增长栈指针的方式在栈中给局部变量分配一些空间。因为一个函数可能在任何时候被调用,此时的栈指针的位置并不容易(完全不能)被预测。因此,我们不能在编译时就分配一个固定的内存地址。唯一能使用的方法就是在程序运行时根据栈指针来实时计算地址。
上述原理不止针对于AVR GCC,也适用于PC电脑。
Flash中的程序 - 初始化.data段
Disassembly of section .text:
00000000 <__ctors_end>:
0: 10 e0 ldi r17, 0x00 ; 0
2: a0 e6 ldi r26, 0x60 ; 96
4: b0 e0 ldi r27, 0x00 ; 0
6: e0 e4 ldi r30, 0x40 ; 64
8: f0 e0 ldi r31, 0x00 ; 0
a: 03 c0 rjmp .+6 ; 0x12 <__zero_reg__+0x11>
c: c8 95 lpm
e: 31 96 adiw r30, 0x01 ; 1
10: 0d 92 st X+, r0
12: a2 36 cpi r26, 0x62 ; 98
14: b1 07 cpc r27, r17
16: d1 f7 brne .-12 ; 0xc <__zero_reg__+0xb>
重启后,内存中的内容是未知的。但是,在主函数main()中,我们期望静态变量a、c的值为我们定义的初始值。因此,就需要有一个步骤来为它们赋值。AVR-GCC会将初始值编译在ROM中,并在程序初始化时将它们复制到RAM中。
在这个例子中,这些值被储存在程序地址0x0040(为什么是这个地址?见程序中储存的数据),.data段的地址位于RAM空间0x60(包括)到0x62(不包括)。在一个涵盖整个.data段的for循环中,通过特殊的lpm(Load program memory)指令使用地址指针Z(R31:R30)从程序空间读取数据(注意,这个指令只能使用数据指针Z),并通过地址指针X(R27:R26)向数据空间写入数据。我们可以使用下面的伪代码来说明这个过程:
uint8_t* ptr_data = &(data_segment);
uint8_t* ptr_program = &(init_value_in_program);
while (ptr_data < &(data_segment_end)) {
*ptr_data = *ptr_program;
ptr_data++;
ptr_program++;
}
要使用8位元的计算单元(ALU)来比较16位元的地址,AVR需要先比较地址的低位,再比较高位。在这个例子中,因为在编译时我们就知道地址了,所以可以将地址低位编译在指令中使用cpi(Compare with immediate)指令。要比较地址高位,我们将需要同时也考虑低位比较时的进位,因为AVR并没有可以将地址编译进指令的进位比较指令,我们只能使用cpc(Compare with register with carry),地址高位先写入寄存器R17。
Flash中的程序 - 初始化.bss段
00000018 <__do_clear_bss>:
18: 20 e0 ldi r18, 0x00 ; 0
1a: a2 e6 ldi r26, 0x62 ; 98
1c: b0 e0 ldi r27, 0x00 ; 0
1e: 01 c0 rjmp .+2 ; 0x22 <.do_clear_bss_start>
00000020 <.do_clear_bss_loop>:
20: 1d 92 st X+, r1
00000022 <.do_clear_bss_start>:
22: a4 36 cpi r26, 0x64 ; 100
24: b2 07 cpc r27, r18
26: e1 f7 brne .-8 ; 0x20 <.do_clear_bss_loop>
.bss段中包含非初始化的值,因为我们期望这些变量为0,AVR-GCC也很贴心地为我们做了清零操作。
在这个例子中,.bss段的地址位于RAM空间0x62(包括)到0x64(不包括)。在一个涵盖整个.bss段的for循环中,通过地址指针X(R27:R26)向数据空间写入零寄存器(R1)的值。我们可以使用下面的伪代码来说明这个过程:
uint8_t* ptr_bss = &(bss_segment);
while (ptr_data < &(bss_segment_end)) {
*ptr_bss = 0;
ptr_bss++;
}
在重启后,所有的寄存器的值都是0.所以,我们可以安全地使用R1作为零寄存器(前提是没被污染的话)。
Flash中的程序 - 用户程序
void main() {
volatile char z = 'Z';
}
00000028 <main>:
28: cf 93 push r28
2a: df 93 push r29
2c: 1f 92 push r1
2e: cd b7 in r28, 0x3d ; 61
30: de b7 in r29, 0x3e ; 62
32: 8a e5 ldi r24, 0x5A ; 90
34: 89 83 std Y+1, r24 ; 0x01
36: 00 00 nop
38: 0f 90 pop r0
3a: df 91 pop r29
3c: cf 91 pop r28
3e: 08 95 ret
我们的主函数。在C语言代码中,我们将字符‘Z’的ASCII码赋值给1字节大小的的局部变量z。
函数内的局部变量被储存在栈中,其地址动态地取决于该函数的框架地址。因此,我们只能通过框架指针来找到它们。AVR并没有原生框架指针,但我们可以通过栈指针的方式来获得框架指针,即,函数初始化所有局部变量后的栈指针所包含的地址就是框架地址。我们将此时的栈指针的值写入一个指针寄存器,就可以将这个寄存器当作框架指针使用了。
在这个例子中,指针寄存器Y(R29:R28)就是框架指针。下面是逐步操作的内容:
- 将当前的指针寄存器Y入栈以保存其内容。R29和R28为函数需要保存的寄存器(Call-Saved Registers)。
- 将随机一个寄存器入栈。具体是那个寄存器无所谓,只要这个入栈操作能占用1字节的栈空间就行了。
- 将栈指针(IO地址
0x3E:0x3D)复制进指针寄存器Y。因为AVR的栈指针指向下一个可用地址且栈向下生长,此时栈指针所指向的地址比变量z实际要低1字节。 - 将字符‘Z’的ASCII码赋值给寄存器R24。接下来,将R24写入Y指针寄存器
+1的地址。 - 在函数末尾,通过将1字节出栈写入一个可以被使用的的寄存器(这里是R0)的方式来销毁1字节的栈空间,并恢复指针寄存器Y,最后返回。
寄存器布局
AVR-GCC ABI (Application Binary Interface),即程序二进制接口规范指出:
固定寄存器(fixed Registers)将不会被函数使用。主要是生成的汇编代码内部使用寄存器R0与R1。
函数用寄存器(call-used or call-clobbered)通用寄存器(GPR)的值可以被函数复写并不需要恢复。
剩下的就是函数保存寄存器(call-saved)。如果一个函数使用了这些寄存器,就必须在返回前恢复其值。即使这些寄存器被用来传递参数。
程序中储存的数据
之前提到,AVR-GCC会将初始值编译在ROM中,并在程序初始化时将它们复制到RAM中。AVR-GCC会将这些内容放在.text段的末尾。在这个例子中,最后一条指令位于程序地址0x003E,因此,.data段的初始内容将在这之后,也就是程序地址0x0040。注意,程序地址必须以16比特对齐,因为AVR的指令都是2字节或4字节长度的。
反编译的目标文件并不会展示.data段的初始内容。不过,我们可以将目标文件转换为16进制文件(也就是烧录单片机时的内容)。
avr-objcopy -O ihex avr-hello.o avr-hello.hex
这将会把目标文件avr-hello.o转化为ihex(Intel十六进制)文件avr-hello.hex。
打开十六进制文件avr-hello.hex可见:
:1000000010E0A0E6B0E0E0E4F0E003C0C89531966F
:100010000D92A236B107D1F720E0A2E6B0E001C010
:100020001D92A436B207E1F7CF93DF931F92CDB7AD
:10003000DEB78AE5898300000F90DF91CF910895A4
:0200400041433A
:00000001FF
倒数第二行写道:0x02个字节位于地址0x0040。第一个字节是0x41,也就是静态变量a的内容‘A’的ASCII编码。第二个字节是0x43,也就是静态变量c的内容‘C’的ASCII编码。此行的校验和为0x3A。
针对特定型号单片机编译
上面的例子中,我们针对通常意义的AVR进行了编译。但实际上,当我们对现实世界的一块单片机编程,并向要实际看到一些输出时,我们需要指定我们实际使用的单片机的型号。
下面,我们将写一个简单的IO操作的程序,并对我在另一篇博客中提到的我最喜欢的,也是最小的,经典AVR单片机ATtiny25编程。该单片机共有6个IO引脚、128字节数据内存(RAM)与2k字节程序闪存(Flash)。
t25-hello.c
#include <avr/io.h>
volatile char a = 'A';
volatile char b;
void main() {
volatile char z = 'Z';
DDRB = 0b00111111;
PORTB = 0b00111111;
}
和上一个C语言程序类似,在main()函数前,我们建立了一个有初始值的静态变量volatile char a = ‘A’和一个没有初始值的静态变量volatile char b。在main()函数中,我们将一个局部变量赋值volatile z = ‘Z’。另外,我们将0b00111111写入IO寄存器DDRB,将所有IO引脚设置为输出。将0b00101010写入IO寄存器PORTB,将部分引脚设置为输出高电平,其余为低电平。
因为每一个型号的单片机的IO各不相同(通用计算机没有IO,不同的单片机有不同的IO),我们需要使用特殊的头文件avr/io.h。
avr-gcc -O0 t25-hello.c -mmcu=attiny25 -o t25-hello.o
avr-objdump -m avr25 -Dsx t25-hello.o
编译源代码t25-hello.c为目标文件t25-hello.o。这一次,我们使用-mmcu=attiny25参数指定单片机架构(型号),以便于编译器正确查找IO寄存器的地址、内存大小、支持指令集(ISA)。
反编译目标文件t25-hello.o中的所有章节。反编译器需要我们指定目标文件的架构,这里我们使用-m avr25参数。具体架构名可以参考AVR-GCC manual。
为了获得详细信息,使用-Dxs三个参数:
下面展示了目标文件的内容,让我们逐步分析:
静态数据
volatile char a = 'A';
volatile char b;
SYMBOL TABLE:
00800060 l d .data 00000000 .data
00800062 l d .bss 00000000 .bss
00800062 g O .bss 00000001 b
00800060 g O .data 00000001 a
上面展示了符号表中的部分内容。ATtiny25的.data段地址从96(十六进制0x60)开始,之后则是在地址0x62开始的.bss段。
虽然我们只有一个需要初始值的静态变量,AVR-GCC为.data段分配了2字节空间。我们也可以在头文件中确认.data段的大小:
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000082 00000000 00000000 00000094 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000002 00800060 00000082 00000116 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000001 00800062 00800062 00000118 2**0
ALLOC
如果我们定义了2个需要初始值的静态变量,.data段的大小就会是2字节。如果我们定义了3个需要初始值的静态变量,.data段的大小就会是4字节。总之,AVR-GCC会以2字节对齐.data段。相反,.bss段不需要对齐。这是大概是因为.data段的初始值被储存在程序空间中,而程序是2字节对其的。
Flash中的程序 - 向量表
Disassembly of section .text:
00000000 <__vectors>:
0: 0e c0 rjmp .+28 ; 0x1e <__ctors_end>
2: 26 c0 rjmp .+76 ; 0x50 <__bad_interrupt>
4: 25 c0 rjmp .+74 ; 0x50 <__bad_interrupt>
6: 24 c0 rjmp .+72 ; 0x50 <__bad_interrupt>
8: 23 c0 rjmp .+70 ; 0x50 <__bad_interrupt>
a: 22 c0 rjmp .+68 ; 0x50 <__bad_interrupt>
c: 21 c0 rjmp .+66 ; 0x50 <__bad_interrupt>
e: 20 c0 rjmp .+64 ; 0x50 <__bad_interrupt>
10: 1f c0 rjmp .+62 ; 0x50 <__bad_interrupt>
12: 1e c0 rjmp .+60 ; 0x50 <__bad_interrupt>
14: 1d c0 rjmp .+58 ; 0x50 <__bad_interrupt>
16: 1c c0 rjmp .+56 ; 0x50 <__bad_interrupt>
18: 1b c0 rjmp .+54 ; 0x50 <__bad_interrupt>
1a: 1a c0 rjmp .+52 ; 0x50 <__bad_interrupt>
1c: 19 c0 rjmp .+50 ; 0x50 <__bad_interrupt>
当发生一个中断(或者说事件)时(比如说,上电、看门狗超时、计时器溢出、模数转换器ADC完成),如果有那个中断的处理程序(ISR)并且那个事件被允许中断的话,硬件就会将程序跳转到那个中断的处理程序的地址。
在这个例子中,位于程序地址0x0000的第一个向量就是复位向量。这个向量指向位于程序地址0x001E的_ctros_end。接下来的14个向量包括了像是时钟中断、数模转换器中断、通讯中断,都指向了程序地址0x0050的__bad_interrupt。这是一个特殊的给未定义的中断的占位符,将重启程序。
如果我们设定了单片机型号,AVR-GCC就会生成一个完整的向量表,即使很多中断并未被使用。就像是这个例子中,只有第一个reset中断向量被使用,AVR-GCC仍然为其余14个向量保留了空间。如果我们自己来link这个程序,去掉这14个未使用的向量,我们就可以获得一些额外的程序空间。
AVR-GCC从提前写好的avr-gnu/avr/lib/[family]/[tiny-stack/]crt[machine].o中获得每个型号单片机的中断表。其中family就是架构,tiny-machine指内存小于256字节只需要1字节栈指针的小单片机,machine则是具体型号。在这个例子中,我们使用avr25架构的、128字节内存空间的、ATting25单片机,因此,我们可以在avr-gnu/avr/lib/avr25/tiny-stack/crtattiny25.o文件中找到我们这款单片机的向量表。通过avr-objdump -m avr25 -dx crtattiny25.o指令可得:
SYMBOL TABLE:
00000000 g .vectors 00000000 __vectors
00000000 w .text 00000000 __vector_1
00000000 g .text 00000000 __bad_interrupt
00000000 w .text 00000000 __vector_2
00000000 w .text 00000000 __vector_3
00000000 w .text 00000000 __vector_4
00000000 w .text 00000000 __vector_5
00000000 w .text 00000000 __vector_6
00000000 w .text 00000000 __vector_7
00000000 w .text 00000000 __vector_8
00000000 w .text 00000000 __vector_9
00000000 w .text 00000000 __vector_10
00000000 w .text 00000000 __vector_11
00000000 w .text 00000000 __vector_12
00000000 w .text 00000000 __vector_13
00000000 w .text 00000000 __vector_14
Disassembly of section .vectors:
00000000 <__vectors>:
0: 00 c0 rjmp .+0 ; 0x2 <__vectors+0x2>
0: R_AVR_13_PCREL __init
2: 00 c0 rjmp .+0 ; 0x4 <__vectors+0x4>
2: R_AVR_13_PCREL __vector_1
4: 00 c0 rjmp .+0 ; 0x6 <__vectors+0x6>
4: R_AVR_13_PCREL __vector_2
6: 00 c0 rjmp .+0 ; 0x8 <__vectors+0x8>
6: R_AVR_13_PCREL __vector_3
8: 00 c0 rjmp .+0 ; 0xa <__vectors+0xa>
8: R_AVR_13_PCREL __vector_4
a: 00 c0 rjmp .+0 ; 0xc <__vectors+0xc>
a: R_AVR_13_PCREL __vector_5
c: 00 c0 rjmp .+0 ; 0xe <__vectors+0xe>
c: R_AVR_13_PCREL __vector_6
e: 00 c0 rjmp .+0 ; 0x10 <__vectors+0x10>
e: R_AVR_13_PCREL __vector_7
10: 00 c0 rjmp .+0 ; 0x12 <__vectors+0x12>
10: R_AVR_13_PCREL __vector_8
12: 00 c0 rjmp .+0 ; 0x14 <__vectors+0x14>
12: R_AVR_13_PCREL __vector_9
14: 00 c0 rjmp .+0 ; 0x16 <__vectors+0x16>
14: R_AVR_13_PCREL __vector_10
16: 00 c0 rjmp .+0 ; 0x18 <__vectors+0x18>
16: R_AVR_13_PCREL __vector_11
18: 00 c0 rjmp .+0 ; 0x1a <__vectors+0x1a>
18: R_AVR_13_PCREL __vector_12
1a: 00 c0 rjmp .+0 ; 0x1c <__vectors+0x1c>
1a: R_AVR_13_PCREL __vector_13
1c: 00 c0 rjmp .+0 ; 0x1e <__FUSE_REGION_LENGTH__+0x1b>
1c: R_AVR_13_PCREL __vector_14
这个提前写好的目标文件提供了一系列的弱向量的符号。如果用户(强)定义了一个中断程序(例如 ISR (TIMER0_COMPA_vect) {...}),AVR-GCC就会在该向量的位置写入用户提供的中断程序的地址。否则,AVR-GCC就会使用弱符号__bad_interrupt的地址。
相对跳转rjmp指令长16比特,其中包含12-bit正负地址差,可以覆盖整个4K程序空间。像是ATting25、ATtiny84这类程序小单片机,只需要相对跳转可以覆盖整个程序空间。因此,每个向量都是1个指令长。而对于程序空间大于4K的其它单片机,像是ATmega328,就需要使用32-bit的jmp指令(22-bit绝对地址,适用4M程序空间)。因此,这些单片机上每个向量都是2个指令长。
Flash中的程序 - 初始化栈与零寄存器
0000001e <__ctors_end>:
1e: 11 24 eor r1, r1
20: 1f be out 0x3f, r1 ; 63
22: cf ed ldi r28, 0xDF ; 223
24: cd bf out 0x3d, r28 ; 61
归零寄存器R1。AVR-GCC使用R1作为零寄存器,该寄存器的值将总是0,便于做一些和0有关的计算,像是比较和相加。
重置SREG(状态寄存器,IO地址0x3F)。
设置SP(栈指针,IO地址0x3E:0x3D)为SRAM的顶端(对于ATtiny25,0x00DF)。对于内存空间大于256字节的单片机,还需要设置SP高位。
AVR-GCC从提前写好的目标文件avr-gnu/avr/lib/[family]/[tiny-stack/]crt[machine].o中复制以下内容:
Disassembly of section .init2:
00000000 <.init2>:
0: 11 24 eor r1, r1
2: 1f be out 0x3f, r1 ; 63
4: c0 e0 ldi r28, 0x00 ; 0
4: R_AVR_LO8_LDI __stack
6: cd bf out 0x3d, r28 ; 61
AVR-LibC Memory Sections指出,这段代码是用来在用户没有特定指出时,初始化栈与零寄存器。In C programs, weakly bound to initialize the stack, and to clear zero_reg (r1).
Flash中的程序 - 初始化.data与.bss段
00000026 <__do_copy_data>:
26: 10 e0 ldi r17, 0x00 ; 0
28: a0 e6 ldi r26, 0x60 ; 96
2a: b0 e0 ldi r27, 0x00 ; 0
2c: e2 e8 ldi r30, 0x82 ; 130
2e: f0 e0 ldi r31, 0x00 ; 0
30: 02 c0 rjmp .+4 ; 0x36 <__do_copy_data+0x10>
32: 05 90 lpm r0, Z+
34: 0d 92 st X+, r0
36: a2 36 cpi r26, 0x62 ; 98
38: b1 07 cpc r27, r17
3a: d9 f7 brne .-10 ; 0x32 <__do_copy_data+0xc>
0000003c<__do_clear_bss>:
3c: 20 e0 ldi r18, 0x00 ; 0
3e: a2 e6 ldi r26, 0x62 ; 98
40: b0 e0 ldi r27, 0x00 ; 0
42: 01 c0 rjmp .+2 ; 0x46 <.do_clear_bss_start>
00000044 <.do_clear_bss_loop>:
44: 1d 92 st X+, r1
00000046 <.do_clear_bss_start>:
46: a3 36 cpi r26, 0x63 ; 99
48: b2 07 cpc r27, r18
4a: e1 f7 brne .-8 ; 0x44 <.do_clear_bss_loop>
将静态变量默认值复制到.data段,清零静态变量.bss段。
Flash中的程序 - 进入执行
4c: 02 d0 rcall .+4 ; 0x52 <main>
4e: 17 c0 rjmp .+46 ; 0x7e <_exit>
00000050 <__bad_interrupt>:
50: d7 cf rjmp .-82 ; 0x0 <__vectors>
初始化完成,调用位于程序地址0x0052的用户主函数。当用户主函数返回后,跳转到位于程序地址0x007E的死循环。
AVR-GCC从提前写好的目标文件avr-gnu/avr/lib/[family]/[tiny-stack/]crt[machine].o中复制以下内容:
Disassembly of section .init9:
00000000 <.init9>:
0: 00 d0 rcall .+0 ; 0x2 <.init9+0x2>
0: R_AVR_13_PCREL main
2: 00 c0 rjmp .+0 ; 0x4 <__FUSE_REGION_LENGTH__+0x1>
2: R_AVR_13_PCREL exit
AVR-LibC Memory Sections指出,这段代码用于跳转到用户主函数。Jumps into main().
如果未定义的中断发生,转战到复位向量.
Flash中的程序 - 用户程序代码
void main() {
volatile char z = 'Z';
DDRB = 0b00111111;
PORTB = 0b00111111;
}
00000052 <main>:
52: cf 93 push r28
54: df 93 push r29
56: 1f 92 push r1
58: cd b7 in r28, 0x3d ; 61
5a: dd 27 eor r29, r29
5c: 8a e5 ldi r24, 0x5A ; 90
5e: 89 83 std Y+1, r24 ; 0x01
60: 87 e3 ldi r24, 0x37 ; 55
62: 90 e0 ldi r25, 0x00 ; 0
64: 2f e3 ldi r18, 0x3F ; 63
66: fc 01 movw r30, r24
68: 20 83 st Z, r18
6a: 88 e3 ldi r24, 0x38 ; 56
6c: 90 e0 ldi r25, 0x00 ; 0
6e: 2f e3 ldi r18, 0x3F ; 63
70: fc 01 movw r30, r24
72: 20 83 st Z, r18
74: 00 00 nop
76: 0f 90 pop r0
78: df 91 pop r29
7a: cf 91 pop r28
7c: 08 95 ret
我们执行了以下项目:
- 保存需要备份的寄存器。在栈为局部变量创建空间。
- 将指令(立刻寻址)中包含的‘Z’(ASCII编码
0x5A)写入寄存器R24。接下来使用指针寄存器Y(R29:R28)作为框架指针,将这个值存入局部变量。 - 将指令中的
0b00111111 (0x3F)写入寄存器R18。使用寄存器R25:R24将DDRB (0x0037)的数据空间地址写入指针寄存器Z(R31:R30),再使用指针Z写R18的值到DDRB。 - 将指令中的
0b00111111 (0x3F)写入寄存器R18。使用寄存器R25:R24将PORTB (0x0038)的数据空间地址写入指针寄存器Z(R31:R30),再使用指针Z写R18的值到PORTB。 - 恢复寄存器的值,销毁栈,返回。
在ATtiny25上,DDRB和PORTBIO地址分别为0x17和0x18,内存地址分别为0x37和0x38。
Flash中的程序 - 末尾
0000007e <_exit>:
7e: f8 94 cli
00000080 <__stop_program>:
80: ff cf rjmp .-2 ; 0x80 <__stop_program>
禁用全局中断。使用死循环停止程序。
开启优化编译
在这个例子中,我们没有使用任何优化。可以说,生成的代码一点也不优雅。
- 如果我们不去碰那些需要备份的寄存器,就能少用一些栈空间并且不用额外的指令执行那些入栈操作。此外,AVR不仅可以用指针寄存器Y(R29:R28)来执行带偏移量的内存操作,还可以使用Z(R31:R30)。
- IO寄存器(这个例子中
DDRB和PORTB)的地址都是固定的,我们可以直接写入而不是通过指针写。例如,可以使用sts memory_address(DDRB), Rr指令。 - 可以IO地址空间寻址的IO寄存器(ATtiny25这类的单片机,所有的IO寄存器的地址都在64字节IO空间内,所有IO寄存器都可以直接IO寻址。其它像是ATmega328这类单片机,有一些IO、寄存器在64字节IO空间以外,这些超出的IO寄存器就只能通过内存空间寻址)。我们可以使用IO地址空间寻址指令,例如
out DDRB, Rr,执行速度更快,占用程序空间更小。 - 为什么非要把地址先写入R25:R24后再复制入指针Z?明明可以直接写入Z。
- 如果一个值已经在寄存器中,就不需要重复装入了。这个例子中,每一次赋值都会重新装入寄存器。
avr-gcc -O1 t25-hello.c -mmcu=attiny25 -o t25-hello.o
avr-objdump -Dsx t25-hello.o
接下来,我们打开基础优化(-O1)来编译。main()函数的反编译结果变为如下:
void main() {
volatile char z = 'Z';
DDRB = 0b00111111;
PORTB = 0b00111111;
}
00000052 <main>:
52: cf 93 push r28
54: df 93 push r29
56: 1f 92 push r1
58: cd b7 in r28, 0x3d ; 61
5a: dd 27 eor r29, r29
5c: 8a e5 ldi r24, 0x5A ; 90
5e: 89 83 std Y+1, r24 ; 0x01
60: 8f e3 ldi r24, 0x3F ; 63
62: 87 bb out 0x17, r24 ; 23
64: 88 bb out 0x18, r24 ; 24
66: 0f 90 pop r0
68: df 91 pop r29
6a: cf 91 pop r28
6c: 08 95 ret
可以看到,AVR-GCC仍然使用需要备份的指针寄存器Y(R29:R28)作为框架指针,如果能用指针寄存器Z的话就更好了。除此之外,通过优化,AVR-GCC用上了性能小钢炮out指令来写DDRBPORTB,且没有重复装入值0b00111111。
除了因为主函数大小变化导致的一些地址变动,其余部分保持不变。实际上AVR-GCC也并没有编译这些未改变的内容,而是直接从提前写好的目标文件中复制出来,再加上地址。
函数与中断(ISR Interrupt Service Routine)
先来看一个简单的C语言代码:
routine.c
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
volatile const uint8_t prog_data[] PROGMEM = {2, 3, 5, 7};
volatile uint8_t static_d1 = 11, static_d2;
void func(uint8_t dir, uint8_t mask) {
volatile uint8_t _mask = 0b00111111;
DDRB = _mask & mask & dir;
}
void main() {
func(0b01010101, 0xFF);
static_d2 = pgm_read_byte(&(prog_data[2]));
for(;;);
}
ISR (WDT_vect) {
PINB = 0b00111111;
}
我们在程序空间内创建了一个uint8_t(8比特无符号整数)类型的数组prog_data。这里使用PROGMEM关键字告诉编译器不要把这个组组放在RAM数据中,而是在闪存中。我们将其填充内容四个质数。
我们再创建两个静态变量:uint8_t static_d1,初始值1,位于.data段。uint8_t static_d2,无初始值,位于.bss段。
子函数func()接受两个参数:uint8_t dir和uint8_t mask。在这个函数内,我们创建一个局部变量uint8_t _mask,并赋值0b00111111。一切正常的话,这个局部变量应该被保存在栈内,并通过框架指针读写。我们将会把这个局部变量与两个函数参数比特求和后写入IO寄存器DDRB。
在main()函数中,我们首先调用子函数func()并传入两个参数:0b01010101为dir、0xFF为mask。接下来,我们将闪存程序空间内数组prog_data的第2个项目(如果从1开始数的话就是第3个)写入RAM内存数据空间局部变量。最后,我们使用死循环for(;;);来终止程序执行。
另外,我们为WDT(看门狗超时)创建中断。如果我们没有周期性的按时复位看门狗,这个中断将就会发生。通常来说,我们应该在程序开头启用看门狗,并且在程序内加入一些看门狗重置指令wdr。这里为了简化,我们先忽略这部分。在这个中断程序中,我们向IO寄存器PINB写入一些值。
avr-gcc -O1 routine.c -o routine.o
avr-objdump -m avr25 -Dxs routine.o
接下来,编译这个程序,只使用基础的优化,可以得到:
静态变量
volatile uint8_t static_d1 = 11, static_d2;
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000000b4 00000000 00000000 00000094 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000002 00800060 000000b4 00000148 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000001 00800062 00800062 0000014a 2**0
ALLOC
SYMBOL TABLE:
00800060 l d .data 00000000 .data
00800062 l d .bss 00000000 .bss
00800062 g O .bss 00000001 static_d2
00800060 g O .data 00000001 static_d1
上面展示了符号表和段落表的部分内容。
静态变量static_d1为1字节长,并被分配在.data段。因为对齐,.data段总共占用2字节空间,位于RAM最前面,即ATtiny25的地址0x60。
.data段后是.bss段,位于地址0x62,长1字节,内容包括1字节长的静态变量static_d2。
Flash中的程序 - 向量表
Disassembly of section .text:
00000000 <__vectors>:
0: 10 c0 rjmp .+32 ; 0x22 <__ctors_end>
2: 28 c0 rjmp .+80 ; 0x54 <__bad_interrupt>
4: 27 c0 rjmp .+78 ; 0x54 <__bad_interrupt>
6: 26 c0 rjmp .+76 ; 0x54 <__bad_interrupt>
8: 25 c0 rjmp .+74 ; 0x54 <__bad_interrupt>
a: 24 c0 rjmp .+72 ; 0x54 <__bad_interrupt>
c: 23 c0 rjmp .+70 ; 0x54 <__bad_interrupt>
e: 22 c0 rjmp .+68 ; 0x54 <__bad_interrupt>
10: 21 c0 rjmp .+66 ; 0x54 <__bad_interrupt>
12: 20 c0 rjmp .+64 ; 0x54 <__bad_interrupt>
14: 1f c0 rjmp .+62 ; 0x54 <__bad_interrupt>
16: 1e c0 rjmp .+60 ; 0x54 <__bad_interrupt>
18: 36 c0 rjmp .+108 ; 0x86 <__vector_12>
1a: 1c c0 rjmp .+56 ; 0x54 <__bad_interrupt>
1c: 1b c0 rjmp .+54 ; 0x54 <__bad_interrupt>
向量表。第一个向量为复位向量,指向程序空间地址0x0022的__ctors_end。其余向量则指向程序空间地址0x0054的__bad_interrupt。几乎和上一个例子相同,除了中断12,指向__vector_12,因为我们提供了看门狗超时的中断程序。
符号表里面也写道:
SYMBOL TABLE:
00000054 w .text 00000000 __vector_1
00000094 g F .text 0000001c __vector_12
00000054 g .text 00000000 __bad_interrupt
00000054 w .text 00000000 __vector_6
00000054 w .text 00000000 __vector_3
00000054 w .text 00000000 __vector_11
00000054 w .text 00000000 __vector_13
00000054 w .text 00000000 __vector_7
00000054 w .text 00000000 __vector_5
00000054 w .text 00000000 __vector_4
00000054 w .text 00000000 __vector_9
00000054 w .text 00000000 __vector_2
00000054 w .text 00000000 __vector_8
00000054 w .text 00000000 __vector_14
00000054 w .text 00000000 __vector_10
因为我们在C源代码中写了ISR (WDT_vect) {...},__vector_12从一个弱符号变成了强符号。
程序数据
volatile const uint8_t prog_data[] PROGMEM = {2, 3, 5, 7};
0000001e <__trampolines_end>:
1e: 02 03 mulsu r16, r18
20: 05 07 cpc r16, r21
使用PROGMEM关键字保存在程序空间中的数据。在这个例子中,我们创建了数组volatile const uint8_t prog_data[] PROGMEM = {2, 3, 5, 7};,这里就包含了这个数组的值。这里只需要关注二进制编码,反编译出来的指令并没有实际意义。
Address of the program data can be found in the symbol table:
SYMBOL TABLE:
0000001e g O .text 00000004 prog_data
Flash中的程序 - 初始化栈与零寄存器
00000022 <__ctors_end>:
22: 11 24 eor r1, r1
24: 1f be out 0x3f, r1 ; 63
26: cf ed ldi r28, 0xDF ; 223
28: cd bf out 0x3d, r28 ; 61
归零寄存器R1。重置SREG(状态寄存器,IO地址0x3F)。设置SP(栈指针,IO地址0x3E:0x3D)为SRAM的顶端(对于ATtiny25,0x00DF)。
Flash中的程序 - 初始化.data与.bss段
0000002a <__do_copy_data>:
2a: 10 e0 ldi r17, 0x00 ; 0
2c: a0 e6 ldi r26, 0x60 ; 96
2e: b0 e0 ldi r27, 0x00 ; 0
30: e4 eb ldi r30, 0xB4 ; 180
32: f0 e0 ldi r31, 0x00 ; 0
34: 02 c0 rjmp .+4 ; 0x3a <__do_copy_data+0x10>
36: 05 90 lpm r0, Z+
38: 0d 92 st X+, r0
3a: a2 36 cpi r26, 0x62 ; 98
3c: b1 07 cpc r27, r17
3e: d9 f7 brne .-10 ; 0x36 <__do_copy_data+0xc>
00000040 <__do_clear_bss>:
40: 20 e0 ldi r18, 0x00 ; 0
42: a2 e6 ldi r26, 0x62 ; 98
44: b0 e0 ldi r27, 0x00 ; 0
46: 01 c0 rjmp .+2 ; 0x4a <.do_clear_bss_start>
00000048 <.do_clear_bss_loop>:
48: 1d 92 st X+, r1
0000004a <.do_clear_bss_start>:
4a: a3 36 cpi r26, 0x63 ; 99
4c: b2 07 cpc r27, r18
4e: e1 f7 brne .-8 ; 0x48 <.do_clear_bss_loop>
将静态变量默认值复制到.data段,清零静态变量.bss段。
Flash中的程序 - 进入执行
50: 11 d0 rcall .+34 ; 0x74 <main>
52: 2e c0 rjmp .+92 ; 0xb0 <_exit>
00000054 <__bad_interrupt>:
54: d5 cf rjmp .-86 ; 0x0 <__vectors>
初始化完成,调用位于程序地址0x0074的用户主函数。当用户主函数返回后,跳转到位于程序地址0x00B0的死循环。
如果未定义的中断发生,转战到复位向量.
Flash中的程序 - 子函数
在我的另一篇博客《AVR调用栈》中详细讨论了AVR-GCC在调用子程序时的栈操作。
void func(uint8_t dir, uint8_t mask) {
volatile uint8_t _mask = 0b00111111;
DDRB = _mask & mask & dir;
}
00000056 <func>:
56: cf 93 push r28
58: df 93 push r29
5a: 1f 92 push r1
5c: cd b7 in r28, 0x3d ; 61
5e: dd 27 eor r29, r29
60: 9f e3 ldi r25, 0x3F ; 63
62: 99 83 std Y+1, r25 ; 0x01
64: 99 81 ldd r25, Y+1 ; 0x01
66: 89 23 and r24, r25
68: 68 23 and r22, r24
6a: 67 bb out 0x17, r22 ; 23
6c: 0f 90 pop r0
6e: df 91 pop r29
70: cf 91 pop r28
72: 08 95 ret
执行以下操作:
- 保存需要备份的寄存器,在栈内为局部变量创空间。
- 将
0b00111111 (0x3F)从指令中写入寄存器R25,接下来用作为框架指针的指针寄存器Y(R29:R28)把这个值写入局部变量_mask。 - 此时局部变量
_mask的值仍在寄存器R25中,我们可以直接与R24中的dir和R22中的mask比特求和,结果写入R22。最后,写入位于IO地址0x17的IO寄存器DDRB。 - 恢复需要备份的寄存器,销毁栈,返回。
Flash中的程序 - 主函数
void main() {
func(0b01010101, 0xFF);
volatile uint8_t temp = pgm_read_byte(&(prog_data[2]));
static_d2 = temp;
for(;;);
}
00000074 <main>:
74: cf 93 push r28
76: df 93 push r29
78: 1f 92 push r1
7a: cd b7 in r28, 0x3d ; 61
7c: dd 27 eor r29, r29
7e: 6f ef ldi r22, 0xFF ; 255
80: 85 e5 ldi r24, 0x55 ; 85
82: e9 df rcall .-46 ; 0x56 <func>
84: e0 e2 ldi r30, 0x20 ; 32
86: f0 e0 ldi r31, 0x00 ; 0
88: e4 91 lpm r30, Z
8a: e9 83 std Y+1, r30 ; 0x01
8c: 89 81 ldd r24, Y+1 ; 0x01
8e: 80 93 62 00 sts 0x0062, r24 ; 0x800062 <__data_end>
92: ff cf rjmp .-2 ; 0x92 <__DATA_REGION_LENGTH__+0x12>
执行以下操作:
- 保存需要备份的寄存器,在栈内为局部变量创空间。
- 将栈指针复制到指针寄存器Y(R29:R28),最为框架指针。
- 将参数
dir写入R24,参数mask写入R22,调用子函数func()。 - 将程序数据中的
prog_data的地址写入指针寄存器Z(R31:R30).接下来,使用lpm Rd, Z来将数据从程序空间读入R30。最后,写入栈内的局部变量temp。 - 将局部变量
temp读入R24,再写入内存空间地址0x0062的静态变量static_d2。 - 使用死循环终止程序。
Flash中的程序 - 中断程序
在我的另一篇博客《AVR裸中断》中详细讨论了中断与裸中断。
ISR (WDT_vect) {
PINB = 0b00111111;
}
00000094 <__vector_12>:
94: 1f 92 push r1
96: 0f 92 push r0
98: 0f b6 in r0, 0x3f ; 63
9a: 0f 92 push r0
9c: 11 24 eor r1, r1
9e: 8f 93 push r24
a0: 8f e3 ldi r24, 0x3F ; 63
a2: 86 bb out 0x16, r24 ; 22
a4: 8f 91 pop r24
a6: 0f 90 pop r0
a8: 0f be out 0x3f, r0 ; 63
aa: 0f 90 pop r0
ac: 1f 90 pop r1
ae: 18 95 reti
执行以下操作:
- 将SREG和所有会被使用的寄存器全部入栈备份。
- 归零R1。虽然零寄存器R1的值应该总是0,考虑到R1可能被临时覆盖,保险起见还是重新归零。
- 将R24赋值
0x3F,写入IO空间地址0x16的IO寄存器PINB。 - 恢复寄存器的值,销毁栈,返回。
Flash中的程序 - 末尾
000000b0 <_exit>:
b0: f8 94 cli
000000b2 <__stop_program>:
b2: ff cf rjmp .-2 ; 0xb2 <__stop_program>
禁用全局中断。使用死循环停止程序。