浏览AVR汇编语言的“hello world”代码

7

我正在尝试为Arduino Duemilanove (AVR ATmega328P)编写一些汇编语言。 在编译和反汇编C代码的同时学习汇编语言,我得到了以下内容:

(使用AVR_GCC进行编译)

int main() {
  volatile int a = 0;
  while (1) {
    ++a;
  }
  return 0;
}

这将转化为

00000000 <__vectors>:
   0: 0c 94 34 00   jmp 0x68  ; 0x68 <__ctors_end>
   4: 0c 94 51 00   jmp 0xa2  ; 0xa2 <__bad_interrupt>
  ...
  64: 0c 94 51 00   jmp 0xa2  ; 0xa2 <__bad_interrupt>

00000068 <__ctors_end>:
  68: 11 24         eor r1, r1
  6a: 1f be         out 0x3f, r1  ; 63
  6c: cf ef         ldi r28, 0xFF ; 255
  6e: d8 e0         ldi r29, 0x08 ; 8
  70: de bf         out 0x3e, r29 ; 62
  72: cd bf         out 0x3d, r28 ; 61

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: e4 ec         ldi r30, 0xC4 ; 196
  7c: f0 e0         ldi r31, 0x00 ; 0
  7e: 02 c0         rjmp  .+4       ; 0x84 <__do_copy_data+0x10>
  80: 05 90         lpm r0, Z+
  82: 0d 92         st  X+, r0
  84: a0 30         cpi r26, 0x00 ; 0
  86: b1 07         cpc r27, r17
  88: d9 f7         brne  .-10      ; 0x80 <__do_copy_data+0xc>

0000008a <__do_clear_bss>:
  8a: 11 e0         ldi r17, 0x01 ; 1
  8c: a0 e0         ldi r26, 0x00 ; 0
  8e: b1 e0         ldi r27, 0x01 ; 1
  90: 01 c0         rjmp  .+2       ; 0x94 <.do_clear_bss_start>

00000092 <.do_clear_bss_loop>:
  92: 1d 92         st  X+, r1

00000094 <.do_clear_bss_start>:
  94: a0 30         cpi r26, 0x00 ; 0
  96: b1 07         cpc r27, r17
  98: e1 f7         brne  .-8       ; 0x92 <.do_clear_bss_loop>
  9a: 0e 94 53 00   call  0xa6  ; 0xa6 <main>
  9e: 0c 94 60 00   jmp 0xc0  ; 0xc0 <_exit>

000000a2 <__bad_interrupt>:
  a2: 0c 94 00 00   jmp 0 ; 0x0 <__vectors>

000000a6 <main>:
  a6: cf 93         push  r28
  a8: df 93         push  r29
  aa: 00 d0         rcall .+0       ; 0xac <main+0x6>
  ac: cd b7         in  r28, 0x3d ; 61
  ae: de b7         in  r29, 0x3e ; 62
  b0: 1a 82         std Y+2, r1 ; 0x02
  b2: 19 82         std Y+1, r1 ; 0x01
  b4: 89 81         ldd r24, Y+1  ; 0x01
  b6: 9a 81         ldd r25, Y+2  ; 0x02
  b8: 01 96         adiw  r24, 0x01 ; 1
  ba: 9a 83         std Y+2, r25  ; 0x02
  bc: 89 83         std Y+1, r24  ; 0x01
  be: fa cf         rjmp  .-12      ; 0xb4 <main+0xe>

000000c0 <_exit>:
  c0: f8 94         cli

000000c2 <__stop_program>:
  c2: ff cf         rjmp  .-2       ; 0xc2 <__stop_program>

我试图理解一些问题:

  1. 什么是.-8或类似语法? (例如地址0x98或0xAA)
  2. 在地址80到88的行附近(__do_copy_data的结尾),有些有趣的事情发生了。我觉得这将所有程序代码加载到RAM中,从地址0xC4开始。为什么?
  3. 在__do_clear_bss_start / loop中,我们通过将RAM中的字节设置为0(r1的值)来清除我们刚刚完成的所有工作。为什么?所有这些最终都会调用main。一些常规的解释吗?
  4. 为什么反汇编不显示.bss、.rodata或其他部分?
  5. 第6a行,为什么要清除SREG?它不应该在每个指令之后设置为所需的值吗?
  6. 第6c和6e行:0xFF和0x08代表什么?r28和r29是堆栈指针低位和高位。
  7. 我玩了一下并添加了一个静态全局变量。为什么我们从0x0100而不是0x0000开始存储在RAM中?
  8. 在第8a行,为什么是ldi r17, 1?我们之前就这样做了(只是无聊的备注)。或者是否有其他东西可以更改r17?
  9. 我们开始将程序从flash复制到RAM,从0xC4开始(我猜.bss和其他部分),但X与1的cpi / cpc将使所有flash都复制到所有RAM中。编译器不停止复制.bss部分时是否只是因为懒惰?

你的链接脚本是什么样子的?如果没有指定,那么在链接时是否使用了“-Tbss”和“-Tdata”选项? - Michael
我使用了 avr-gcc -Wall -Os -DF_CPU=$(CLOCK) -mmcu=$(DEVICE) -o main.elf main.oavr-objcopy -j .text -j .data -O ihex main.elf main.hex - Thomas
编辑,它显示的内容(.text、.bss、.rodata)可能与您反汇编的方式有关,我使用whatever-objdump -D,通常可以解决问题。 - old_timer
从你那里的内容看,似乎在0x0000处有一个向量表,所以你不能只是把数据放在那里,我需要重新熟悉avr的细节才能说更多。基本上,我认为代码必须在那里,.bss和.data或其他内容的放置部分取决于它们在链接器脚本或命令行中的描述位置。 - old_timer
ldi r17,1 在这里出现两次很可能是因为这两个循环是用汇编语言编写的,而编写者可能并不关心优化,或者由于宏等原因。如果我写的话,我会保留 ldi,因为这两个循环是独立的代码,你可以删除其中一个而不会影响另一个。 - old_timer
2个回答

3
点号/句号被用作快捷方式来指示此指令的地址或位置,或者与此相关的某些内容。.+8表示从这里开始加8。您必须考虑指令集和/或汇编器相对于指令集的细微差别。正如汇编器的附加信息所示,.-8将转到do_clear_bss_loop,包括指令本身的两个字节在内向前八个字节。原始代码可能只是在其中加入了标签brne do_clear_bss_loop
它很可能在复制数据段;.text基本上是只读的。它是你的代码,希望在这个平台上存储在闪存中。然而.data是可读/写的,并且通常初始化为非零值。因此,在关闭电源时,您的初始值需要在某个地方保留,在闪存中,例如,但在启动真正的程序之前,引导程序将需要将初始的.data段值从闪存复制到它们实际的家在RAM中。然后,随着程序运行,它可以根据需要读取和/或修改这些值。
例如:
int x = 5;

main ()
{
    x = x + 1;
}

那个值5必须在flash中才能从断电启动时使用flash保存非易失性信息。但在读/写x的内存位置之前,需要将其放入RAM中,因此一些启动代码将所有.data段数据从flash复制到RAM中。
很抱歉,我对你的问题只是猜测,做了这么长的解释。
.bss是程序中初始化为零的变量。对于.data段,如果有100个项目,我们需要100个flash中的东西。但对于.bss,如果有100个项目,我们只需要告诉某人有100个项目即可,不需要100个零在flash中,只需编译/汇编到代码中即可。
int x = 5;
int y;

int main ()
{
    while(1)
    {
        y = y + x + 1;
    }
}
x.data 中,需要将 5 存储在非易失性存储器中。y.bss 中,在调用主函数之前只需将其清零以符合 C 标准。
即使您自己不使用全局变量,可能仍有其他数据以某种方式使用 .data 和/或 .bss 段,因此引导代码在调用 main() 之前准备了 .data.bss 段,以便您的 C 编程体验与预期相同。

跳转到同一行/指令。无限循环。实际上我认为应该是rjmp .-2(因为“.”似乎是下一条指令的地址)。 - Michael
我可能是在没有检查 ARM 的情况下将其翻译成 AVR,但我手头记不清楚 AVR 是什么了。 - old_timer
我明白了,感谢您的解释。所以这些操作都是为了将数据复制到内存中,并将未初始化的内容清零。我会在测试程序上尝试一下。顺便说一句,在C语言中,未初始化的值不应该被期望为零。 - Thomas
可能或取决于标准和语言(C vs C ++),但未初始化的全局变量应该为零,这是我听说过的,但我自己没有确认过。我的个人裸机代码没有这些假设,在使用变量之前在代码中初始化变量,我的引导程序只设置堆栈指针并跳转到主函数,避免了这些复制循环、链接器脚本等等来使它们都能正常工作。在github.com/dwelch67/raspberrypi的bssdata目录中,我通过一些例子展示了从gnu角度看所有这些是如何工作的(不幸的是这是特定于工具链的)。 - old_timer
我已经阅读了你在Github上的链接,非常有用并且解释得很清楚。谢谢。 - Thomas
嗨,经过更深入的思考,我已经编辑了问题,因为我有一些更多的问题,我认为它们也适合在这个帖子中讨论。谢谢! - Thomas

3

我知道这是一个晚回答,但我仍然认为有一个详细的逐点回答所有问题可能很有趣。

  1. .-8或类似语法是什么?(例如地址0x98或0xAA。)

它的意思是:“从这里跳回8个字节”。请注意,程序计数器已经增加了指令的长度(2个字节),因此brne .-8将使您在brne指令本身之前移动6个字节(而不是8个字节)。同样,rcall .+0会将程序计数器推入堆栈而不会改变程序流程。这是一个技巧,只旨在在单个指令中保留两个字节的堆栈空间。

  1. 在地址80到88行附近(__do_copy_data的末尾)有一些有趣的事情。我觉得这会将所有程序代码从地址0xC4加载到RAM中。为什么?

不,没有复制任何内容,这是一个空循环。在84到88行,有一个测试,当指针X(r27:r26)等于0x0100时退出循环。由于X被初始化为0x0100,因此根本不会循环。

这个循环旨在将数据部分从闪存复制到RAM。它基本上做了这样的事情:

X = DATA_START;  // RAM address
Z = 0x00C4;      // Flash address
while (X != DATA_START + DATA_SIZE)
    ram[X++] = flash[Z++];

但是你的程序恰好有一个空数据部分(DATA_SIZE == 0)。

此外,你应该注意到你的程序在地址0x00c3结束,因此Z指针被初始化为指向程序代码之后。这是初始化变量初始值应该在的地方。

  1. 在__do_clear_bss_start/loop中,我们通过将RAM中的字节设置为0(r1的值)来清除我们刚刚完成的所有工作。为什么?所有这些最终都是为了调用main。有一般的解释吗?

不,没有任何内容会被覆盖。此循环清除BSS,通常在数据部分之后,没有重叠。伪代码:

X = BSS_START;
while (X != BSS_START + BSS_SIZE)
    ram[X++] = 0;

BSS_START == DATA_START + DATA_SIZE 时,这也是你的程序中一个空循环,因为你有一个空的 bss。

  1. 为什么反汇编不显示 .bss、.rodata 或其他部分?

因为 objdump -d 只反汇编预期保存代码的部分。

  1. 第6a行,为什么清除 SREG?每个指令后它不应该被设置为它应该的值吗?

大多数指令只修改 SREG 的一些位。此外,这会清除全局中断使能位。

  1. 第6c和6e行:0xFF 和 0x08 对应什么?r28 和 r29 是堆栈指针低位和高位。

堆栈指针加载了 0x08ff,这是 ATmega328P 中最后一个 RAM 位置。堆栈将从那里向下增长。

  1. 我尝试添加了一个静态全局变量。为什么我们从 0x0100 而不是 0x0000 开始存储在 RAM 中?

RAM 在 328P 上的地址为 0x0100–0x08ff。在此地址以下,您有一些内存映射寄存器(CPU 寄存器和 I/O 寄存器)。请查看数据手册中的详细信息,第 8.3 节 SRAM 数据存储器。

  1. 第8a行,为什么是 ldi r17, 1?我们之前已经做过了(只是一个愚蠢的备注)。或者其他东西可以改变 r17 吗?

第8a行是无用的。它在这里是因为链接器通过将不同的部分粘合在一起来构建程序的方式:__do_copy_data 和 __do_clear_bss 是独立的例程,它们不依赖于其他留在寄存器中的内容。

  1. 我们开始将程序从闪存复制到 RAM,从 0xC4 开始(.bss 和其他部分),但是 X 的 cpi/cpc 与 1 相关,会使所有闪存都复制到所有 RAM 中。编译器是否只是因为懒惰而没有在 .bss 部分完成复制时停止复制?
您误解了代码的这部分。cpi、cpc和brne指令只会在X与r17:0x00(即0x0100,因为r17=1)不同时循环。请参见上面的伪代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接