AVR-GCC生成代码

这一篇文章将通过一些列不同的的示例代码来探讨AVR-GCC生成的代码。

--by Captdam @ Nov 4, 2025 Nov 1, 2025

[en] Here is the English version of this article

【中】 这里是这篇文章的中文版

Index

最基础 - 通常意义上的AVR

我们先来看一个简单的C语言代码:


avr-hello.c

volatile char a = 'A', b, c = 'C', d;

void main() {
	volatile char z = 'Z';
}
	

在这个例子中,我们创建了4个全局变量abcd,并对其中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个参数获得我们需要的所有内容:

下面展示了对象文件的内容,让我们逐步分析:

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个初始化的静态变量ac,都是char类型,各自使用1字节空间。因此,.data段占用2字节空间,从0x60(包括)到0x62(不包括)。接下来是2个未初始化的静态变量bd,都是char类型,各自使用1字节空间。因此,.bss段占用2字节空间,从0x62(包括)到0x64(不包括)。

虽然我们声明b早于c,符号表中c的地址却先于b的地址。AVR-GCC修改了变量的地址以便于将同一个段的变量放在一起。

RAM map of a device with internal RAM
AVR-LibC Manual - Memory Areas and Using malloc() - https://www.nongnu.org/avr-libc/user-manual/malloc.html

静态(全局)变量与堆(局部)变量

对于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()中,我们期望静态变量ac的值为我们定义的初始值。因此,就需要有一个步骤来为它们赋值。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)就是框架指针。下面是逐步操作的内容:

  1. 将当前的指针寄存器Y入栈以保存其内容。R29和R28为函数需要保存的寄存器(Call-Saved Registers)。
  2. 将随机一个寄存器入栈。具体是那个寄存器无所谓,只要这个入栈操作能占用1字节的栈空间就行了。
  3. 将栈指针(IO地址0x3E:0x3D)复制进指针寄存器Y。因为AVR的栈指针指向下一个可用地址且栈向下生长,此时栈指针所指向的地址比变量z实际要低1字节。
  4. 将字符‘Z’的ASCII码赋值给寄存器R24。接下来,将R24写入Y指针寄存器+1的地址。
  5. 在函数末尾,通过将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
		

我们执行了以下项目:

  1. 保存需要备份的寄存器。在栈为局部变量创建空间。
  2. 将指令(立刻寻址)中包含的‘Z’(ASCII编码0x5A)写入寄存器R24。接下来使用指针寄存器Y(R29:R28)作为框架指针,将这个值存入局部变量。
  3. 将指令中的0b00111111 (0x3F)写入寄存器R18。使用寄存器R25:R24将DDRB (0x0037)的数据空间地址写入指针寄存器Z(R31:R30),再使用指针Z写R18的值到DDRB
  4. 将指令中的0b00111111 (0x3F) 写入寄存器R18。使用寄存器R25:R24将PORTB (0x0038)的数据空间地址写入指针寄存器Z(R31:R30),再使用指针Z写R18的值到PORTB
  5. 恢复寄存器的值,销毁栈,返回。

在ATtiny25上,DDRBPORTBIO地址分别为0x170x18,内存地址分别为0x370x38

Flash中的程序 - 末尾


0000007e <_exit>:
	7e:	f8 94       	cli

00000080 <__stop_program>:
	80:	ff cf       	rjmp	.-2      	; 0x80 <__stop_program>
	

禁用全局中断。使用死循环停止程序。

开启优化编译

在这个例子中,我们没有使用任何优化。可以说,生成的代码一点也不优雅。


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 diruint8_t mask。在这个函数内,我们创建一个局部变量uint8_t _mask,并赋值0b00111111。一切正常的话,这个局部变量应该被保存在栈内,并通过框架指针读写。我们将会把这个局部变量与两个函数参数比特求和后写入IO寄存器DDRB

main()函数中,我们首先调用子函数func()并传入两个参数:0b01010101dir0xFFmask。接下来,我们将闪存程序空间内数组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
		

执行以下操作:

  1. 保存需要备份的寄存器,在栈内为局部变量创空间。
  2. 0b00111111 (0x3F)从指令中写入寄存器R25,接下来用作为框架指针的指针寄存器Y(R29:R28)把这个值写入局部变量_mask
  3. 此时局部变量_mask的值仍在寄存器R25中,我们可以直接与R24中的dir和R22中的mask比特求和,结果写入R22。最后,写入位于IO地址0x17的IO寄存器DDRB
  4. 恢复需要备份的寄存器,销毁栈,返回。

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>
		

执行以下操作:

  1. 保存需要备份的寄存器,在栈内为局部变量创空间。
  2. 将栈指针复制到指针寄存器Y(R29:R28),最为框架指针。
  3. 将参数dir写入R24,参数mask写入R22,调用子函数func()
  4. 将程序数据中的prog_data的地址写入指针寄存器Z(R31:R30).接下来,使用lpm Rd, Z来将数据从程序空间读入R30。最后,写入栈内的局部变量temp
  5. 将局部变量temp读入R24,再写入内存空间地址0x0062的静态变量static_d2
  6. 使用死循环终止程序。

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
		

执行以下操作:

  1. 将SREG和所有会被使用的寄存器全部入栈备份。
  2. 归零R1。虽然零寄存器R1的值应该总是0,考虑到R1可能被临时覆盖,保险起见还是重新归零。
  3. 将R24赋值0x3F,写入IO空间地址0x16的IO寄存器PINB
  4. 恢复寄存器的值,销毁栈,返回。

Flash中的程序 - 末尾


000000b0 <_exit>:
	b0:	f8 94       	cli

000000b2 <__stop_program>:
	b2:	ff cf       	rjmp	.-2      	; 0xb2 <__stop_program>
	

禁用全局中断。使用死循环停止程序。