我将通过举例来说明,尽管标签为x86,但ARM更易于阅读等方面 - 功能上是相同的。
引导程序
.globl _start
_start:
ldr r0,=__bss_start__
ldr r1,=__bss_end__
mov r2,#0
bss_fill:
cmp r0,r1
beq bss_fill_done
strb r2,[r0],#1
b bss_fill
bss_fill_done:
/* data copy would go here */
bl main
b .
这段代码可能存在漏洞,效率肯定不高,但是为了演示目的而存在。
C 代码
unsigned int ba;
unsigned int bb;
unsigned int da=5;
unsigned int db=0x12345678;
int main ( void )
{
ba=5;
bb=0x88776655;
return(0);
}
我也可以使用汇编语言,但是在汇编语言中,.bss、.data等不像在编译后的代码中那么有意义。
MEMORY
{
rom : ORIGIN = 0x08000000, LENGTH = 0x1000
ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > rom
.rodata : { *(.rodata*) } > ram
__bss_start__ = .;
.bss : { *(.bss*) } > ram
__bss_end__ = .;
__data_start__ = .;
.data : { *(.data*) } > ram
__data_end__ = .;
}
使用的链接脚本。
结果:
Disassembly of section .text:
08000000 <_start>:
8000000: e59f001c ldr r0, [pc, #28]
8000004: e59f101c ldr r1, [pc, #28]
8000008: e3a02000 mov r2, #0
0800000c <bss_fill>:
800000c: e1500001 cmp r0, r1
8000010: 0a000001 beq 800001c <bss_fill_done>
8000014: e4c02001 strb r2, [r0], #1
8000018: eafffffb b 800000c <bss_fill>
0800001c <bss_fill_done>:
800001c: eb000002 bl 800002c <main>
8000020: eafffffe b 8000020 <bss_fill_done+0x4>
8000024: 08000058 stmdaeq r0, {r3, r4, r6}
8000028: 20000008 andcs r0, r0, r8
0800002c <main>:
800002c: e3a00005 mov r0, #5
8000030: e59f1014 ldr r1, [pc, #20]
8000034: e59f3014 ldr r3, [pc, #20]
8000038: e59f2014 ldr r2, [pc, #20]
800003c: e5810000 str r0, [r1]
8000040: e5832000 str r2, [r3]
8000044: e3a00000 mov r0, #0
8000048: e12fff1e bx lr
800004c: 20000004 andcs r0, r0, r4
8000050: 20000000 andcs r0, r0, r0
8000054: 88776655 ldmdahi r7!, {r0, r2, r4, r6, r9, r10, sp, lr}^
Disassembly of section .bss:
20000000 <bb>:
20000000: 00000000 andeq r0, r0, r0
20000004 <ba>:
20000004: 00000000 andeq r0, r0, r0
Disassembly of section .data:
20000008 <db>:
20000008: 12345678 eorsne r5, r4, #120, 12
2000000c <da>:
2000000c: 00000005 andeq r0, r0, r5
显然,在最后你可以看到四个变量的存储,它们是.bss和.data,正如预期的那样。
但这里有一个人们试图解释的区别。
应该有代码来清零.bss,这是一种浪费周期的做法,一些编译器开始警告使用未初始化的变量,这是好的,但无论如何,.bss都有一些清零代码。 .data也可能有一些复制代码,我没有完成这个示例以展示它是如何工作的,您可以告诉链接器脚本,.data在ram中,但将其副本放在rom中,并具有两个地址和大小/结束,rom数据开始和ram数据开始,然后从rom复制到ram。
因此,.data与.bss的成本差异在于,对于.data,您已经分配了内存,通过操作系统加载程序或自己的引导程序,该数据可能需要再次复制,也可能不需要。
20000008 <db>:
20000008: 12345678
对于.bss
20000000 <bb>:
20000000: 00000000 andeq r0, r0, r0
再次提到操作系统加载程序和/或构建方法(在这种情况下,将 .data 放在 .bss 之后,并且至少有一个 .data 项,如果你将其 objcopy -O binary,你会得到在 .bin 文件中被清零的数据,而不需要填充那些 .bss 数据,这取决于加载程序和目标文件)。
因此,存储空间是相等的,但是 .bss 的额外成本是
800002c: e3a00005 mov r0, #5
8000030: e59f1014 ldr r1, [pc, #20]
800003c: e5810000 str r0, [r1]
800004c: 20000004
并且
8000034: e59f3014 ldr r3, [pc, #20]
8000038: e59f2014 ldr r2, [pc, #20]
8000040: e5832000 str r2, [r3]
8000050: 20000000
8000054: 88776655
第一个需要一条指令将5存入寄存器,一条指令获取地址和一个内存周期将5存入内存。第二个更昂贵,因为它需要一条带有内存周期的指令来获取数据,然后是一条获取地址的指令,最后是存储,所有这些都是内存周期。另一个答案试图争辩说你没有静态成本,因为它们是立即数,但是变长指令集中的这些立即数是存在并像固定长度一样从内存中读取的,它不是单独的内存周期,而是预取的一部分,但它仍然是静态存储。区别在于您至少需要一个内存周期将值存储在内存中(.bss和.data意味着全局,因此需要将其存储到内存中)。由于这些是链接的,变量的地址需要由链接器放置在固定长度risc指令集附近的池中,在类似x86的cisc中,它将嵌入到移动立即数到寄存器中,无论哪种方式,地址的静态存储和值的静态存储,x86与arm相比,x86将使用更少的字节的指令执行两个指令的任务,arm需要三个指令和三个单独的内存周期。功能上是相同的。现在,在完全控制下(裸机)违反期望但可以节省您的开销。
.globl _start
_start:
ldr sp,=0x20002000
bl main
b .
unsigned int ba
unsigned int bb
int main ( void )
{
ba=5
bb=0x88776655
return(0)
}
MEMORY
{
rom : ORIGIN = 0x08000000, LENGTH = 0x1000
ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > rom
.rodata : { *(.rodata*) } > ram
.bss : { *(.bss*) } > ram
}
Disassembly of section .text:
08000000 <_start>:
8000000: e59fd004 ldr sp, [pc, #4]
8000004: eb000001 bl 8000010 <main>
8000008: eafffffe b 8000008 <_start+0x8>
800000c: 20002000 andcs r2, r0, r0
08000010 <main>:
8000010: e3a00005 mov r0, #5
8000014: e59f1014 ldr r1, [pc, #20]
8000018: e59f3014 ldr r3, [pc, #20]
800001c: e59f2014 ldr r2, [pc, #20]
8000020: e5810000 str r0, [r1]
8000024: e5832000 str r2, [r3]
8000028: e3a00000 mov r0, #0
800002c: e12fff1e bx lr
8000030: 20000004 andcs r0, r0, r4
8000034: 20000000 andcs r0, r0, r0
8000038: 88776655 ldmdahi r7!, {r0, r2, r4, r6, r9, r10, sp, lr}^
Disassembly of section .bss:
20000000 <bb>:
20000000: 00000000 andeq r0, r0, r0
20000004 <ba>:
20000004: 00000000 andeq r0, r0, r0
我认为在之前的示例中删除了堆栈初始化。
没有必要使(特定于工具链的)链接器脚本复杂化,也没有必要在引导程序中初始化任何内存,而是在代码中初始化变量。虽然这样做在 .text 空间方面更加昂贵,但更易于编写和维护,并且如果需要,更容易移植等。但是,如果有人想拿这段代码并添加一个 .data 项或假设一个 .bss 项被清零,则会打破已知的规则/假设。
另一个快捷方式,比如树莓派裸机:
.globl _start
_start:
ldr sp,=0x8000
bl main
b .
unsigned int ba
unsigned int bb
unsigned int da=5
int main ( void )
{
return(0)
}
MEMORY
{
ram : ORIGIN = 0x00008000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > ram
.rodata : { *(.rodata*) } > ram
.bss : { *(.bss*) } > ram
.data : { *(.data*) } > ram
}
Disassembly of section .text:
00008000 <_start>:
8000: e3a0d902 mov sp, #32768
8004: eb000000 bl 800c <main>
8008: eafffffe b 8008 <_start+0x8>
0000800c <main>:
800c: e3a00000 mov r0, #0
8010: e12fff1e bx lr
Disassembly of section .bss:
00008014 <bb>:
8014: 00000000 andeq r0, r0, r0
00008018 <ba>:
8018: 00000000 andeq r0, r0, r0
Disassembly of section .data:
0000801c <da>:
801c: 00000005 andeq r0, r0, r5
hexdump -C so.bin
00000000 02 d9 a0 e3 00 00 00 eb fe ff ff ea 00 00 a0 e3 |................|
00000010 1e ff 2f e1 00 00 00 00 00 00 00 00 05 00 00 00 |../.............|
00000020
存在一个 .data 项和在链接脚本中定义 .bss 之后以及二进制文件作为整体被GPU复制到RAM中,包括 .text、.bss、.data 等。.bss 的清零是一个免费的好处,我们不需要为 .bss 添加额外的代码,如果我们有更多的 .data 并且正在使用它,我们也会得到一个免费的 .data 初始化/复制。
这些都是边角情况,但确实展示了你所考虑的为什么要将变量清零,我可以随后在 .text 中更改或最终更改的原因。这也延伸到为什么要在启动时烧录清零该部分,为什么要复杂化链接器脚本,GNU 链接器脚本最好是让人头痛的,必须非常小心才能正确使用,当然,一旦你弄对了,每个工具链项目的每个版本都不需要太多工作来查看它是否仍然有效。
为了正确地执行,.bss会消耗指令和执行时间,包括单独的内存总线周期。但是无论如何,对于.bss都应该有链接器脚本和引导代码。同样对于.data也是如此,但除非基于rom/flash,否则.data的源和目标很可能是相同的,复制发生在加载程序中(操作系统将二进制文件从rom/flash/disk复制到内存),并且不需要额外的复制,除非你在链接器脚本中强制执行它。
根据其他问题中的评论,“正确”是基于假设的,.data项目需要按编译代码中定义的方式显示,历史上发现.bss是特定于工具链的,规范说什么我需要查找,使用的工具链的版本可能会因为并非所有今天使用的工具链都在不断维护以符合当前的标准而不同。一些人有限制其项目为那些具有最新工具的奢侈品,而许多人则没有。
这里展示的快捷方式类似于手动调整汇编语言而不是使用编译器提供的内容,你需要自己决定,如果不小心可能会有风险,但是如果项目需要,可以在启动时获得相当可观的性能提升。对于非专业工作,不建议使用此类方法。请注意,使用全局变量也会引起使用者的争议。如果不使用全局变量,则仍需处理本地全局变量,即我所说的本地静态变量,它们属于此类别。
unsigned int more_fun ( unsigned int, unsigned int )
void fun ( unsigned int x )
{
static int ba
static int da=0x12345678
ba+=x
da=more_fun(ba,da)
}
int main ( void )
{
return(0)
}
0000800c <fun>:
800c: e59f2028 ldr r2, [pc, #40]
8010: e5923000 ldr r3, [r2]
8014: e92d4010 push {r4, lr}
8018: e59f4020 ldr r4, [pc, #32]
801c: e0803003 add r3, r0, r3
8020: e5941000 ldr r1, [r4]
8024: e1a00003 mov r0, r3
8028: e5823000 str r3, [r2]
802c: ebfffff6 bl 800c <fun>
8030: e5840000 str r0, [r4]
8034: e8bd4010 pop {r4, lr}
8038: e12fff1e bx lr
803c: 0000804c andeq r8, r0, r12, asr #32
8040: 00008050 andeq r8, r0, r0, asr r0
00008044 <main>:
8044: e3a00000 mov r0, #0
8048: e12fff1e bx lr
Disassembly of section .bss:
0000804c <ba.3666>:
804c: 00000000 andeq r0, r0, r0
Disassembly of section .data:
00008050 <da.3667>:
8050: 12345678 eorsne r5, r4, #120, 12
无论是本地静态变量还是本地全局变量,它们仍然会存储在.data或.bss中。