嵌入式系统:使用汇编语言时的内存布局

Eri*_*ric 2 embedded microcontroller assembly bare-metal

根据我的理解,嵌入式系统运行机器代码。有多种方法可以生成此代码。一种是用 C 等高级语言编写程序并使用编译器来获取此类代码。另一种方法是用汇编语言为该嵌入式系统编写指令,并使用汇编程序将其转换为机器代码。现在我们得到了加载到系统并执行的机器代码。程序代码存储在非易失性存储器中。

现在,如果程序代码是从 C 编译器获得的,我知道以下内容:代码包含多个部分:

  • .text: 实际指令
  • .bss:已声明但未定义的变量
  • .data:声明和定义的变量
  • .rodata:声明和定义的只读变量(“const”)

然后,在启动时 .bss 和 .data 被(在大多数情况下)加载到 ram 中。然后,堆栈指针放置在数据段之后,堆指针放置在 ram 的末尾,以便在执行过程中,它们再次相互增长。

已编译程序的内存布局

现在的问题是,如果我用汇编语言编写代码,事情会怎样?根据我的理解,应该没有像上面那样的部分(在程序代码或 ram 中),只有代码(相当于 .text)。我可以手动访问内存地址并从那里写入和读取,但没有堆栈和堆之类的东西。这种描绘是否正确?

old*_*mer 6

您的图表是事物的教科书视图,不一定是不正确的,但对于微控制器而言,事物的外观并不完全正确。

C 和汇编语言产生相同的结果,一般来说,一个对象包含机器代码和数据以及一些结构,以便链接器知道什么是什么。包括某种信息来指示哪些字节块是什么,通常称为节。具体名称 .text、.data 等不是一成不变的,工具开发人员可以自由选择他们想要的任何名称。如果他们不使用这些名称,那么就会给习惯于这些术语的一般人群增加混淆。因此,即使您可能正在编写一个新的编译器,因为您不喜欢任何现有的编译器,所以在一定程度上遵守是明智的。

堆栈指针与处理器中的任何其他寄存器/概念一样有用,与语言无关。大多数处理器都受到通用寄存器数量的限制,因此有时您需要暂时节省一些以腾出空间来做更多的工作。子程序/函数的概念需要某种带有返回概念的跳转。独立于编程语言(这意味着包括汇编语言,它是一种编程语言)。

堆是一种在操作系统或您无法完全控制的环境中运行的概念。您所谈论的关于微控制器的内容称为裸机编程。这通常意味着没有操作系统。这意味着/意味着您可以完全控制。你不必要求记忆,你只需要把它拿走。

对于一般的微控制器(几乎所有这些语句都有例外),存在某种形式的非易失性存储器(闪存、eeprom 等,某种 rom)和 ram(sram)。芯片供应商为特定芯片或芯片系列选择这些逻辑组件的地址空间。处理器内核本身很少关心,它们只是地址。程序员负责连接所有的点。因此,MCU 内存模型将有一个闪存地址空间,是的,它基本上包含代码和理想情况下的只读项(程序员需要告诉工具执行此操作)。并且 sram 将具有读/写项目。但是还存在另一个问题。所谓的 .data 项希望在代码主体之前或在 C 语言编译代码开始执行之前的情况下设置为一个值。同样如果 。bss 被假定为零,这也必须发生。这是在有时称为引导程序中完成的。一些(理想情况下)汇编语言代码,可以在应用程序的入口点和高级语言 (C) 的入口点之间架起桥梁。操作系统首先支持有限数量的二进制格式文件类型。然后在这些操作系统中,操作系统作者决定是否要为您准备内存,而不仅仅是为您的应用程序分配空间,通常都是 ram,您没有我将要描述的 MCU 问题。操作系统可以简单地将数据放置在链接的位置,并将零 .bss 放置在链接的位置。一些(理想情况下)汇编语言代码,可以在应用程序的入口点和高级语言 (C) 的入口点之间架起桥梁。操作系统首先支持有限数量的二进制格式文件类型。然后在这些操作系统中,操作系统作者决定是否要为您准备内存,而不仅仅是为您的应用程序分配空间,通常都是 ram,您没有我将要描述的 MCU 问题。操作系统可以简单地将数据放置在链接的位置,并将零 .bss 放置在链接的位置。一些(理想情况下)汇编语言代码,可以在应用程序的入口点和高级语言 (C) 的入口点之间架起桥梁。操作系统首先支持有限数量的二进制格式文件类型。然后在这些操作系统中,操作系统作者决定是否要为您准备内存,而不仅仅是为您的应用程序分配空间,通常都是 ram,您没有我将要描述的 MCU 问题。操作系统可以简单地将数据放置在链接的位置,并将零 .bss 放置在链接的位置。然后在这些操作系统中,操作系统作者决定是否要为您准备内存,而不仅仅是为您的应用程序分配空间,通常都是 ram,您没有我将要描述的 MCU 问题。操作系统可以简单地将数据放置在链接的位置,并将零 .bss 放置在链接的位置。然后在这些操作系统中,操作系统作者决定是否要为您准备内存,而不仅仅是为您的应用程序分配空间,通常都是 ram,您没有我将要描述的 MCU 问题。操作系统可以简单地将数据放置在链接的位置,并将零 .bss 放置在链接的位置。

使用 MCU,您通常会启动处理器,您的代码是第一个代码,没有操作系统为您准备和管理事物,这对 IMO 来说很好,但也意味着更多的工作。具体而言,您在启动时所拥有的只是非易失性存储,为了将 .data 项放入 ram,您需要在 rom 中拥有它们的副本,并且您需要在执行任何假定它们处于最终状态的编译代码之前复制它们地方。这是引导程序的工作之一,另一个是设置堆栈指针,因为编译器在生成编译代码时假设有一个堆栈。

unsigned int a;
unsigned int b = 5;
const unsigned int c = 7;
void fun ( void  )
{
    a = b + c;
}
Disassembly of section .text:

00000000 <fun>:
   0:   e59f3010    ldr r3, [pc, #16]   ; 18 <fun+0x18>
   4:   e5933000    ldr r3, [r3]
   8:   e59f200c    ldr r2, [pc, #12]   ; 1c <fun+0x1c>
   c:   e2833007    add r3, r3, #7
  10:   e5823000    str r3, [r2]
  14:   e12fff1e    bx  lr
    ...

Disassembly of section .data:

00000000 <b>:
   0:   00000005    andeq   r0, r0, r5

Disassembly of section .bss:

00000000 <a>:
   0:   00000000    andeq   r0, r0, r0

Disassembly of section .rodata:

00000000 <c>:
   0:   00000007    andeq   r0, r0, r7
Run Code Online (Sandbox Code Playgroud)

您可以在此示例中看到所有这些元素。

arm-none-eabi-ld -Ttext=0x1000 -Tdata=0x2000 -Tbss=0x3000 -Trodata=0x4000 so.o -o so.elf

Disassembly of section .text:

00001000 <fun>:
    1000:   e59f3010    ldr r3, [pc, #16]   ; 1018 <fun+0x18>
    1004:   e5933000    ldr r3, [r3]
    1008:   e59f200c    ldr r2, [pc, #12]   ; 101c <fun+0x1c>
    100c:   e2833007    add r3, r3, #7
    1010:   e5823000    str r3, [r2]
    1014:   e12fff1e    bx  lr
    1018:   00002000
    101c:   00003000

Disassembly of section .data:

00002000 <b>:
    2000:   00000005

Disassembly of section .bss:

00003000 <a>:
    3000:   00000000

Disassembly of section .rodata:

00001020 <c>:
    1020:   00000007
Run Code Online (Sandbox Code Playgroud)

(自然这不是有效/可执行的二进制文件,工具不知道/关心)

该工具忽略了我的 -Trodata,但您可以看到我们控制了事情的发展方向,我们通常通过链接来做到这一点。我们最终负责确保构建与目标匹配,我们链接事物以匹配芯片地址空间布局。

使用许多编译器,尤其是 gnu GCC,您可以创建汇编语言输出。在 GCC 的情况下,它编译为汇编语言,然后调用汇编程序(一个明智的设计选择,但不是必需的)。

arm-none-eabi-gcc -O2 -save-temps -c so.c -o so.o
cat so.s
    .cpu arm7tdmi
    .eabi_attribute 20, 1
    .eabi_attribute 21, 1
    .eabi_attribute 23, 3
    .eabi_attribute 24, 1
    .eabi_attribute 25, 1
    .eabi_attribute 26, 1
    .eabi_attribute 30, 2
    .eabi_attribute 34, 0
    .eabi_attribute 18, 4
    .file   "so.c"
    .text
    .align  2
    .global fun
    .arch armv4t
    .syntax unified
    .arm
    .fpu softvfp
    .type   fun, %function
fun:
    @ Function supports interworking.
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    ldr r3, .L3
    ldr r3, [r3]
    ldr r2, .L3+4
    add r3, r3, #7
    str r3, [r2]
    bx  lr
.L4:
    .align  2
.L3:
    .word   .LANCHOR1
    .word   .LANCHOR0
    .size   fun, .-fun
    .global c
    .global b
    .global a
    .section    .rodata
    .align  2
    .type   c, %object
    .size   c, 4
c:
    .word   7
    .data
    .align  2
    .set    .LANCHOR1,. + 0
    .type   b, %object
    .size   b, 4
b:
    .word   5
    .bss
    .align  2
    .set    .LANCHOR0,. + 0
    .type   a, %object
    .size   a, 4
a:
    .space  4
    .ident  "GCC: (GNU) 10.2.0"
Run Code Online (Sandbox Code Playgroud)

钥匙就在那里。了解汇编语言特定于汇编程序(程序)而不是目标(CPU/芯片),这意味着您可以为同一个处理器芯片使用许多不兼容的汇编语言,只要它们生成正确的机器代码,它们都是有用的. 这是 gnu 汇编器(gas)汇编语言。

.text
nop
add r0,r0,r1
eor r1,r2
b .
.align
.bss
.word 0
.data
.word 0x12345678
.section .rodata
.word 0xAABBCCDD

Disassembly of section .text:

00000000 <.text>:
   0:   e1a00000    nop         ; (mov r0, r0)
   4:   e0800001    add r0, r0, r1
   8:   e0211002    eor r1, r1, r2
   c:   eafffffe    b   c <.text+0xc>

Disassembly of section .data:

00000000 <.data>:
   0:   12345678

Disassembly of section .bss:

00000000 <.bss>:
   0:   00000000

Disassembly of section .rodata:

00000000 <.rodata>:
   0:   aabbccdd
Run Code Online (Sandbox Code Playgroud)

链接方式相同:

Disassembly of section .text:

00001000 <.text>:
    1000:   e1a00000    nop         ; (mov r0, r0)
    1004:   e0800001    add r0, r0, r1
    1008:   e0211002    eor r1, r1, r2
    100c:   eafffffe    b   100c <__data_start-0xff4>

Disassembly of section .data:

00002000 <__data_start>:
    2000:   12345678

Disassembly of section .bss:

00003000 <__bss_start+0xffc>:
    3000:   00000000

Disassembly of section .rodata:

00001010 <_stack-0x7eff0>:
    1010:   aabbccdd
Run Code Online (Sandbox Code Playgroud)

对于带有 gnu 链接器 (ld) 的 MCU,请注意链接器脚本或您如何告诉链接器您想要的特定于链接器的内容,不要假设它可以以任何方式移植到来自其他工具链的其他链接器。

MEMORY
{
    rom : ORIGIN = 0x10000000, LENGTH = 0x1000
    ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > rom
    .rodata : { *(.rodata*) } > rom
    .data   : { *(.data*)   } > ram AT > rom
    .bss    : { *(.bss*)    } > ram AT > rom
}
Run Code Online (Sandbox Code Playgroud)

我首先告诉链接器我希望在一个地方只读的东西和在另一个地方读/写的东西。请注意,单词 rom 和 ram 仅用于连接点(对于 gnu 链接器):

MEMORY
{
    ted : ORIGIN = 0x10000000, LENGTH = 0x1000
    bob : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > ted
    .rodata : { *(.rodata*) } > ted
    .data   : { *(.data*)   } > bob AT > ted
    .bss    : { *(.bss*)    } > bob AT > ted
}
Run Code Online (Sandbox Code Playgroud)

现在我们得到:

Disassembly of section .text:

10000000 <.text>:
10000000:   e1a00000    nop         ; (mov r0, r0)
10000004:   e0800001    add r0, r0, r1
10000008:   e0211002    eor r1, r1, r2
1000000c:   eafffffe    b   1000000c <.text+0xc>

Disassembly of section .rodata:

10000010 <.rodata>:
10000010:   aabbccdd

Disassembly of section .data:

20000000 <.data>:
20000000:   12345678

Disassembly of section .bss:

20000004 <.bss>:
20000004:   00000000
Run Code Online (Sandbox Code Playgroud)

但!我们有机会通过 MCU 取得成功:

arm-none-eabi-objcopy -O binary so.elf so.bin
hexdump -C so.bin
00000000  00 00 a0 e1 01 00 80 e0  02 10 21 e0 fe ff ff ea  |..........!.....|
00000010  dd cc bb aa 78 56 34 12                           |....xV4.|
00000018

arm-none-eabi-objcopy -O srec --srec-forceS3 so.elf so.srec
cat so.srec
S00A0000736F2E7372656338
S315100000000000A0E1010080E0021021E0FEFFFFEAFF
S30910000010DDCCBBAAC8
S3091000001478563412BE
S70510000000EA
Run Code Online (Sandbox Code Playgroud)

你可以看到 AABBCCDD 和 12345678

S30910000010DDCCBBAAC8 AABBCCDD at address 0x10000010
S3091000001478563412BE 12345678 at address 0x10000014
Run Code Online (Sandbox Code Playgroud)

在闪光。下一步,如果您的链接器可以帮助您,如果不能,那就不好了:

MEMORY
{
    ted : ORIGIN = 0x10000000, LENGTH = 0x1000
    bob : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > ted
    .rodata : { *(.rodata*) } > ted
    __data_rom_start__ = .;
    .data   : 
        {
            __data_start__ = .;
            *(.data*)   
        } > bob AT > ted
    .bss    : 
        { 
            __bss_start__ = .;
            *(.bss*)    
        } > bob AT > ted
}
Run Code Online (Sandbox Code Playgroud)

基本上创建您可以在其他语言中看到的变量/标签:

.text
nop
add r0,r0,r1
eor r1,r2
b .
.align
.word __data_rom_start__
.word __data_start__
.word __bss_start__
.bss
.word 0
.data
.word 0x12345678
.section .rodata
.word 0xAABBCCDD

Disassembly of section .text:

10000000 <.text>:
10000000:   e1a00000    nop         ; (mov r0, r0)
10000004:   e0800001    add r0, r0, r1
10000008:   e0211002    eor r1, r1, r2
1000000c:   eafffffe    b   1000000c <__data_rom_start__-0x14>
10000010:   10000020
10000014:   20000000
10000018:   20000004

Disassembly of section .rodata:

1000001c <__data_rom_start__-0x4>:
1000001c:   aabbccdd

Disassembly of section .data:

20000000 <__data_start__>:
20000000:   12345678

Disassembly of section .bss:

20000004 <__bss_start__>:
20000004:   00000000

S00A0000736F2E7372656338
S315100000000000A0E1010080E0021021E0FEFFFFEAFF
S311100000102000001000000020040000205A
S3091000001CDDCCBBAABC
S3091000002078563412B2
S70510000000EA
Run Code Online (Sandbox Code Playgroud)

工具将 .data 放置在 0x10000020

S3091000002078563412B2
Run Code Online (Sandbox Code Playgroud)

我们在闪光中看到的

10000010: 10000020 __data_rom_start__
10000014: 20000000 __data_start__
10000018: 20000004 __bss_start__

arm-none-eabi-nm so.elf 
20000004 B __bss_start__
10000020 R __data_rom_start__
20000000 D __data_start__
Run Code Online (Sandbox Code Playgroud)

添加更多这些类型的东西(请注意,gnu ld 链接器脚本是使这些东西正确的 PITA),然后您可以编写一些汇编语言代码来将 .data 项复制到 ram,因为您现在知道二进制文件中的位置和链接器在 ram 中放置东西的位置。.bss 在哪里,现在有很多内存需要清除/归零。

裸机中的内存分配是不可取的,通常是因为现在裸机是微控制器类型的工作。不限于此,操作系统本身就是一个裸机程序,由另一个裸机程序bootloader引导。但是对于 MCU,您的资源,尤其是 ram 非常有限,如果您使用全局变量而不是局部变量,并且不动态分配而是静态声明事物,那么使用工具可以看到大部分 sram 使用情况,并且也可以受链接描述文件的限制。

arm-none-eabi-readelf -l so.elf

Elf file type is EXEC (Executable file)
Entry point 0x10000000
There are 2 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x010000 0x10000000 0x10000000 0x00020 0x00020 R E 0x10000
  LOAD           0x020000 0x20000000 0x10000020 0x00004 0x00008 RW  0x10000

 Section to Segment mapping:
  Segment Sections...
   00     .text .rodata 
   01     .data .bss 
Run Code Online (Sandbox Code Playgroud)

通常设置链接描述文件的大小以匹配目标硬件,为了演示目的,这里被夸大了。

bob : ORIGIN = 0x20000000, LENGTH = 0x4

arm-none-eabi-ld -T flash.ld so.o -o so.elf
arm-none-eabi-ld: so.elf section `.bss' will not fit in region `bob'
arm-none-eabi-ld: region `bob' overflowed by 4 bytes
Run Code Online (Sandbox Code Playgroud)

如果您使用过多的动态分配,无论是局部变量还是 malloc() 调用系列,那么您必须进行消耗分析以查看您的堆栈是否溢出到数据中。或者你的数据入栈。这充其量是相当困难的。

还要理解裸机意味着没有操作系统极大地限制了您可以使用的 C 库,因为其中很大一部分依赖于操作系统。特别是一般的 alloc 函数。因此,为了在运行时进行动态内存分配,您需要为实现分配的 C 库实现后端。(提示使用您的链接器脚本找出未使用的 ram 的大小/位置)。因此不鼓励在运行时动态分配内存。但有时您会想要这样做并且需要实施它。

汇编语言显然可以自由地使用堆栈,因为它只是架构的另一部分,并且通常有特定于堆栈的指令也受汇编语言支持。堆和任何其他 C 库语言调用都可以从汇编语言进行,因为根据定义,汇编语言可以像 C 一样调用标签/地址。

unsigned char * fun ( unsigned int x )
{
    return malloc(x);
}

fun:
    push    {r4, lr}
    bl  malloc
    pop {r4, lr}
    bx  lr
Run Code Online (Sandbox Code Playgroud)

.text、.rodata、.data、.bss、stack 和 heap 都可用于汇编语言,至少对于面向目标文件和链接的汇编程序而言。有一些汇编器旨在成为单一文件类型的东西,或者不与对象和链接器一起使用,因此不需要节,而是有类似的东西

.org 0x1000
nop
add r0,r1,r2
.org 0x2000
.word 0x12345678
Run Code Online (Sandbox Code Playgroud)

您在哪里声明事物在汇编语言本身中的特定地址。有些工具可能会让您混合这些概念,但它可能会让您和工具感到非常困惑。

使用大量使用的现代工具,如 gnu/binutils 和 clang/llvm,节的使用/概念可用于所有支持的语言,以及从一个对象到另一个对象的函数/库调用(可以拥有和使用独立的 C 库)用于调用它的语言)。