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

3
据我了解,嵌入式系统运行机器代码。有多种方法可以生成这个代码。一种是使用高级语言(如C语言)编写程序,并使用编译器生成这样的代码。另一种方法是使用该嵌入式系统的汇编语言编写指令,并使用汇编器将其转换为机器代码。现在我们得到了加载到系统并执行的机器代码。程序代码存储在非易失性内存中。
现在,如果程序代码是从C编译器获得的,我知道以下内容: 代码包含多个部分:
.text:实际指令 .bss:声明但未定义变量 .data:声明和定义变量 .rodata:声明和定义只读变量(“const”)
然后,在启动时,.bss和.data通常会被加载到RAM中。 然后,在数据段之后放置堆栈指针,在RAM末尾放置堆指针,以便在执行过程中它们相互增长。

Memory layout of compiled programm

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

2
汇编器通常也会组织各个部分。不要忘记嵌入式系统中可能存在操作系统的可能性。 - undefined
移除了C标签,因为这与C语言无关。 - undefined
阅读您特定系统的ABI - undefined
你怎么会认为“嵌入式”系统中没有RAM部分呢?最近我设计了一个小型CPU,内置了256K个18位字的RAM。而且许多小型演示板都包含DDR RAM。 - undefined
2个回答

7

你的图表是一种教材式的视角,不一定是错误的,但对于微控制器来说并不完全准确。

C语言和汇编语言通常会生成同样的结果,一个包含机器代码、数据和某些结构以供链接器识别的对象。其中会包含指示哪些字节块属于什么的信息,通常称为“section”。具体名称如 .text、.data 等并非铁板钉钉,工具开发者可以自由选择任何名称。如果他们不使用这些名称,则会给习惯于这些术语的普通人造成困惑。因此,尽管您可能正在编写自己喜欢的新编译器,但遵循一定程度的规范是明智的。

堆栈指针与处理器中的任何其他寄存器/概念一样有用,独立于语言。大多数处理器受通用寄存器数量的限制,因此在需要临时保存一些寄存器以腾出更多空间进行工作时,必须做出某些妥协。子程序/函数的概念需要某种跳转以及返回的概念,这与编程语言无关(这意味着包括汇编语言在内)。

堆是在运行操作系统或不完全控制环境时的概念。您所说的关于微控制器的内容称为裸机编程。这通常意味着没有操作系统。这意味着/表示您拥有完全的控制权。您无需请求内存,只需要占用即可。

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

在使用微控制器时,通常需要启动处理器,您的代码是第一段运行的代码,没有操作系统为您准备和管理东西,这在我看来是很好的,但也意味着需要更多的工作。具体而言,在引导时,您只有非易失性存储器(non-volatile storage),为了将 .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

你可以在这个例子中看到所有的元素。
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

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

工具忽略了我的 -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"

这里就涉及到了关键点。理解汇编语言是针对汇编器(程序)而非目标(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

以相同的方式链接:
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

对于带有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
}

我首先告诉链接器,我希望只读内容放在一个地方,可读写内容放在另一个地方。注意,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
}

现在我们得到:
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

但是!我们有一个机会在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

您可以看到AABBCCDD和12345678。

S30910000010DDCCBBAAC8 AABBCCDD at address 0x10000010
S3091000001478563412BE 12345678 at address 0x10000014

在Flash中,如果您的链接器无法帮助您,那么下一步就毫无意义了。
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
}

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

.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

这些工具将 .data 存放在 0x10000020 内存地址

S3091000002078563412B2

我们在闪存中看到的

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__

增加更多这些类型的元素(请注意,GNU ld链接脚本很难正确配置),然后可以编写一些汇编语言代码将.data项复制到RAM中,因为您现在知道链接器将事物放置在二进制文件和RAM中的位置。并且知道.bss在哪里以及需要清除/归零的内存量。
裸机环境中的内存分配并不理想,通常是因为现在的裸机工作是微控制器类型的工作。 它不仅限于此,操作系统本身就是一个裸机程序,由另一个裸机程序引导启动-引导加载程序。 但对于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 

通常将链接器脚本大小设置为与目标硬件匹配,这里为了演示而夸大了。
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

如果您过度使用动态分配,无论是局部变量还是 malloc() 函数族,那么您需要进行消耗分析,以查看堆栈是否溢出到数据。或者数据是否溢出到堆栈。这可能是非常困难的。
另外,要理解没有操作系统的裸机意味着你能使用的 C 库会受到很大限制,因为其中较大比例的功能依赖于操作系统,特别是一般的分配函数。因此,为了在运行时甚至拥有动态内存分配,您需要实现实现分配的 C 库的后端。 (提示:使用链接器脚本查找未使用 RAM 的大小/位置)。因此,在运行时不鼓励使用动态内存分配。但是,在某些情况下,您将希望这样做并需要实现它。
汇编语言显然可以自由地使用堆栈,因为它只是体系结构的另一个部分,并且通常也支持与堆栈相关的指令。可以从汇编语言中调用堆和任何其他 C 库语言调用,因为按定义,汇编语言可以像 C 一样调用标签/地址。
unsigned char * fun ( unsigned int x )
{
    return malloc(x);
}

fun:
    push    {r4, lr}
    bl  malloc
    pop {r4, lr}
    bx  lr

对于至少针对目标文件和链接的汇编器来说,.text、.rodata、.data、.bss、堆栈和堆都可用于汇编语言。也有一些汇编器只用于单个文件类型或不使用对象和链接器,因此不需要节(sections),但会有类似的东西

.org 0x1000
nop
add r0,r1,r2
.org 0x2000
.word 0x12345678

在汇编语言本身中声明事物所在的具体地址。一些工具可能会让你混合使用这些概念,但这可能会让你和工具感到相当困惑。

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


2

通常由你决定。

您的汇编程序将支持节(section),但如果您愿意,可以将所有内容放在一个节中,然后完全忘记节。

大多数CPU都有堆栈,这只是意味着它们具有堆栈指针寄存器和特定的推入和弹出指令。堆栈顶部(最后一个推入的项目)是堆栈指针寄存器所指示的位置。而CPU实际上并不关心底部在哪里。通常,您应该在汇编程序的开头放置一条指令,将堆栈指针设置为特定地址,即您想要堆栈底部的位置。

堆(heap)是由您的程序创建的东西。CPU根本不知道它,汇编器也不知道它。您可能能够链接到C语言的malloc库(汇编程序仍然可以使用库,甚至是用C编写的库)。或者您也可以创建自己的malloc。


你说“汇编器将支持节(section)”。你能再多说一些这些节可以是什么吗? - undefined
关于malloc部分的第二个问题:在微控制器中不鼓励使用动态内存分配。在C语言中,这意味着我们应该尽量避免使用malloc。那么在汇编语言中会意味着什么呢?我并不是指当你再次链接malloc库的情况,而是指“纯粹”的汇编语言情况下。 - undefined
@Eric 嗯,CPU并没有malloc函数。malloc是由某人编写的一个函数,它在编译时查看未使用的内存区域,并允许你在运行时将其标记为已使用或未使用的部分。 - undefined

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