静态C变量未被初始化

3
我有一个文件级别的静态C变量没有被初始化。
const size_t VGA_WIDTH = 80;
const size_t VGA_HEIGHT = 25;
static uint16_t* vgat_buffer = (uint16_t*)0x62414756; // VGAb
static char vgat_initialized= '\0';

特别地,第一次访问vgat_initialized时,它并不总是为0。(当然,这个问题只出现在某些机器上。)
我正在尝试编写自己的操作系统,所以我相信这是我的链接脚本存在问题;但是,我不清楚变量在链接器生成的映像中应该如何组织(即,我不确定这个变量应该放在.data.bss、其他某个部分等等)。 VGA_WIDTHVGA_HEIGHT按预期被放置在.rodata部分。 vgat_buffer按预期被放置在.data部分。(通过将此变量初始化为0x62417656,我可以清楚地看到链接器在生成的映像文件中将其放置在哪里。)
我无法弄清楚vgat_initialized应该放在哪里。我在下面包含了汇编文件的相关部分。据我所知,.comm指令应该在数据段中为变量分配空间,但我无法确定具体位置。查看链接器的映像文件也没有提供任何线索。
有趣的是,如果我改变初始化方式,则...
 static char vgat_initialized= 'x';

一切都按预期工作:我可以清楚地看到变量在生成的图像文件中的位置(即,我可以在图像文件的十六进制转储中看到x)。


C文件生成的汇编代码:

.text
.LHOTE15:
    .local  buffer.1138
    .comm   buffer.1138,100,64
    .local  buffer.1125
    .comm   buffer.1125,100,64
    .local  vgat_initialized
    .comm   vgat_initialized,1,1
    .data
    .align 4
    .type   vgat_buffer, @object
    .size   vgat_buffer, 4
vgat_buffer:
    .long   1648445270
    .globl  VGA_HEIGHT
    .section    .rodata
    .align 4
    .type   VGA_HEIGHT, @object
    .size   VGA_HEIGHT, 4
VGA_HEIGHT:
    .long   25
    .globl  VGA_WIDTH
    .align 4
    .type   VGA_WIDTH, @object
    .size   VGA_WIDTH, 4
VGA_WIDTH:
    .long   80
    .ident  "GCC: (GNU) 4.9.2"

2
可能的变量被初始化为零并放置在内存中,这些内存应该在访问之前或之时填充为零。 - David Schwartz
4
通常情况下,静态变量被初始化为零或默认值时会被分配到.bss段中。通常情况下,这些变量在可执行文件中实际上不占用任何空间;系统预计只需提供足够整个段的零填充空间即可。 - John Bollinger
1
@Zack 负责的人因实现而异。过去是装载器,但现在通常是内核的内存管理系统。(毕竟,当程序开始运行时,几乎没有分配物理内存,所以要清零什么呢?) - David Schwartz
1
如果您编写了自己的引导程序,并且只是使用int 13从磁盘读取内核到内存中,那么您需要自己负责清零内存(如果您使用GRUB / multiboot作为引导程序,则不需要,因为GRUB会为您执行此操作)。最好的方法是在链接器脚本中创建一个BSS部分,其中包含可用于在_C_代码中将BSS区域初始化为零的开始和结束符号。 - Michael Petch
1
如果将BSS段放置在代码和数据的末尾,则它不会占用二进制文件中的任何空间。在未读取扇区和可能也包含BSS段的内存中是否存在零是随机的。最好的方法是使用链接器符号确定BSS段的范围,然后在第一次调用_C_代码之前显式地循环遍历该区域并将其全部设置为零。这将确保BSS实际上为零。 - Michael Petch
显示剩余5条评论
4个回答

4
编译器可以按照它们自己的部分名称进行确认,但使用我们从特定编译器中知道的常见 .data、.text、.rodata、.bss,这应该落在 .bss 中。但这并不意味着自动将其清零。需要有一种机制,有时取决于您的工具链,工具链会处理它并创建一个二进制文件,除了填充 .data、.rodata(和自然的 .text)之外,还将在二进制文件中填充 .bss。但这取决于一些事情,主要是这是否是一个简单的仅 RAM 映像,是否所有内容都在链接脚本中定义的一个内存空间下。例如,您可以在链接器脚本中将 .data 放在 .bss 之后,根据您使用的二进制格式和/或转换工具,您可能会得到二进制文件中的零内存,而无需进行任何其他工作。
通常情况下,您应该期望使用工具链特定的机制(链接器脚本是链接器特定的,不能假定它适用于所有工具)来定义从您的角度来看 .bss 的位置,然后从链接器那里得到某种形式的通信,以确定其开始位置和大小。启动程序使用该信息来将其清零,在这种情况下,可以假设始终是启动程序的工作将 .bss 清零,当然也有一些例外情况。同样,如果二进制文件意味着在只读媒体(ROM、Flash 等)上,但 .data 和 .bss 是可读/写的,则需要将 .data 完全放在该媒体上,然后有人必须将其复制到 RAM 中的运行时位置,并且根据工具链和您使用它的方式,.bss 可能是其中的一部分,或者起始地址和大小位于只读媒体上,有人必须在 pre-main() 的某个时刻将该空间清零。同样,在这里,这是启动程序的工作。设置堆栈指针、移动 .data(如果需要)、清零 .bss 是启动程序的典型最小工作,您可以在特殊情况下快捷地完成它们或避免使用 .data 或 .bss。
由于链接器的工作是从被链接的对象中获取所有小的 .data 和 .bss(以及其他)定义,并根据用户的指示(链接器脚本、命令行、任何该工具使用的内容)将它们组合起来,因此链接器最终知道。
在 gcc 的情况下,您使用我称之为在链接器脚本中定义的变量,链接器脚本可以使用与汇编器匹配的变量/标签名称填充这些值,以便使用通用启动程序,您不必做任何更多的工作。就像这样,但可能更复杂。
MEMORY
{
    bob : ORIGIN = 0x8000, LENGTH = 0x1000
    ted : ORIGIN = 0xA000, LENGTH = 0x1000
}

SECTIONS
{
   .text : { *(.text*) } > bob
   __data_rom_start__ = .;
   .data : {
    __data_start__ = .;
    *(.data*)
   } > ted AT > bob
   __data_end__ = .;
   __data_size__ = __data_end__ - __data_start__;
   .bss  : {
   __bss_start__ = .;
   *(.bss*)
   } > bob
   __bss_end__ = .;
   __bss_size__ = __bss_end__ - __bss_start__;
}

然后,您可以将这些内容拉入汇编语言引导程序中。
.globl bss_start
bss_start: .word __bss_start__
.globl bss_end
bss_end: .word __bss_end__
.word __bss_size__
.globl data_rom_start
data_rom_start:
.word __data_rom_start__
.globl data_start
data_start:
.word __data_start__
.globl data_end
data_end:
.word __data_end__
.word __data_size__

然后编写一些代码来操作这些内容,以满足您的设计需求。

您可以将这样的内容简单地放在一个链接的汇编语言文件中,而不需要其他使用它们的代码,然后进行汇编、编译其他代码和链接,然后您喜欢的反汇编或其他工具将向您展示链接器生成了什么,调整它直到您满意,然后您就可以编写、借用或窃取引导代码来使用它们。

对于裸机,我更喜欢不完全符合标准的代码,没有任何 .data,也不期望 .bss 为零,因此我的引导程序设置堆栈指针并调用 main,完成。对于操作系统,您应该遵守标准。工具链已经解决了本地平台的问题,但如果您要使用自己的链接器脚本和引导程序接管它,则需要处理它,如果您想使用现有工具链的解决方案来使用现有操作系统,则...完成...只需这样做。


对于GNU来说,默认情况下它会以.bss结尾,但你可能可以让它落在其他地方。所以.bss是一个问题的答案之一。上面的内容是关于如何将其清零的,这也许是你问过或者没有问过的问题。 - old_timer

2
这个答案只是其他答案的延伸。如C标准所述,有关初始化的规则:

10) 如果具有自动存储期限的对象没有明确初始化,则其值是不确定的。如果没有明确初始化具有静态存储期限的对象,则:

  • 如果它具有指针类型,则初始化为空指针;
  • 如果它具有算术类型,则将其初始化为(正数或无符号)零;
  • 如果是聚合的,则每个成员都按照这些规则递归地初始化;
  • 如果它是一个联合,则第一个命名成员根据这些规则被初始化(递归)。

您代码中的问题在于计算机的内存可能不总是初始化为零。您需要确保在类似于操作系统和引导加载程序的自由环境中初始化BSS部分为零。

BSS部分通常不会(默认情况下)在二进制文件中占用空间,并且通常会占用超出二进制中出现的代码和数据的限制的区域的内存。这样做是为了减小必须读入内存的二进制文件的大小。

我知道您正在为使用遗留BIOS引导的x86编写操作系统。 我知道你正在使用你其他最近的问题中提到的GCC, 我知道你正在使用GNU汇编器来处理你的引导程序的一部分, 我知道你有一个链接脚本, 但我不知道它看起来像什么。通常通过链接器脚本来实现这一点,该脚本将BSS数据放置在末尾,并创建开始和结束符号以定义该部分的地址范围。 一旦链接器定义了这些符号,就可以由C代码(或汇编代码)使用它们循环遍历该区域并将其设置为零。

我提供了一个相当简单的MCVE来做到这一点。 代码使用Int 13h / AH = 2h读取具有内核的额外扇区; 使用快速A20方法启用A20行; 使用32位描述符加载GDT; 启用受保护模式; 完成过渡到32位受保护模式; 然后调用名为C的内核入口点中的kmainkmain调用名为zero_bssC函数,根据自定义链接器脚本生成的起始和结束符号(__bss_start__bss_end)初始化BSS部分。

boot.S:

.extern kmain
.globl mbrentry
.code16
.section .text

mbrentry:
    # If trying to create USB media, a BPB here may be needed
    # At entry DL contains boot drive number

    # Segment registers to zero
    xor %ax, %ax
    mov %ax, %ds
    mov %ax, %es
    # Set stack to grow down from area under the place the bootloader was loaded
    mov %ax, %ss
    mov $0x7c00, %sp

    cld                  # Ensure forward direction of MOVS/SCAS/LODS instructions
                         #     which is required by generated C code

    # Load kernel into memory
    mov $0x02, %ah       # Disk read
    mov $1, %al          # Read 1 sector
    xor %ch, %ch         # Cylinder 0
    xor %dh, %dh         # Head 0
    mov $2, %cl          # Start reading from second sector
    mov $0x7e00, %bx     # Load kernel at 0x7e00
    int $0x13

    # Quick and dirty A20 enabling. May not work on all hardware
a20fast:
    in  $0x92, %al
    or  $2, %al
    out %al, $0x92

loadgdt:
    cli                  # Turn off interrupts until a Interrupt Vector
                         #     Table (IVT) is set
    lgdt (gdtr)
    mov  %cr0, %eax
    or   $1, %al
    mov  %eax, %cr0      # Enable protected mode
    jmp  $0x08,$init_pm  # FAR JMP to next instruction to set
                         #     CS selector with a 32-bit code descriptor and to
                         #     flush the instruction prefetch queue

.code32
init_pm:
    # Set remaining 32-bit selectors
    mov $DATA_SEG, %ax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    mov %ax, %ss

    # Start executing kernel
    call kmain
    cli
loopend:                # Infinite loop when finished
    hlt
    jmp loopend


.align 8
gdt_start:
    .long 0              # null descriptor
    .long 0

gdt_code:
    .word 0xFFFF         # limit low
    .word 0              # base low
    .byte 0              # base middle
    .byte 0b10011010     # access
    .byte 0b11001111     # granularity/limit high
    .byte 0              # base high

gdt_data:
    .word 0xFFFF         # limit low (Same as code)
    .word 0              # base low
    .byte 0              # base middle
    .byte 0b10010010     # access
    .byte 0b11001111     # granularity/limit high
    .byte 0              # base high
end_of_gdt:

gdtr:
    .word end_of_gdt - gdt_start - 1
                         # limit (Size of GDT)
    .long gdt_start      # base of GDT

    CODE_SEG = gdt_code - gdt_start
    DATA_SEG = gdt_data - gdt_start

kernel.c:

#include <stdint.h>

extern uintptr_t __bss_start[];
extern uintptr_t __bss_end[];

/* Zero the BSS section 4-bytes at a time */
static void zero_bss(void)
{
    uint32_t *memloc = __bss_start;

    while (memloc < __bss_end)
        *memloc++ = 0;
}

int kmain(){
    zero_bss();
    return 0;
}

link.ld

ENTRY(mbrentry)
SECTIONS
{
    . = 0x7C00;
    .mbr : {
        boot.o(.text);
        boot.o(.*);
    }

    . = 0x7dfe;
    .bootsig : {
        SHORT(0xaa55);
    }

    . = 0x7e00;
    .kernel : {
        *(.text*);
        *(.data*);
        *(.rodata*);
    }

    .bss : SUBALIGN(4) {
        __bss_start = .;
        *(COMMON);
        *(.bss*);
    }
    . = ALIGN(4);
    __bss_end = .;

    /DISCARD/ : {
        *(.eh_frame);
        *(.comment);
    }
}

要编译、链接并生成一个可用于磁盘映像的二进制文件,您可以使用如下命令:

as --32 boot.S -o boot.o
gcc -c -m32 -ffreestanding -O3 kernel.c
gcc -ffreestanding -nostdlib -Wl,--build-id=none -m32 -Tlink.ld \
    -o boot.elf -lgcc boot.o kernel.o
objcopy -O binary boot.elf boot.bin

1
C标准规定,即使没有显式初始化,static变量也必须进行零初始化,因此static char vgat_initialized= '\0';等同于static char vgat_initialized;
在ELF和其他类似格式中,诸如vgat_initialized的零初始化数据将进入.bss部分。如果您自己将这样的可执行文件加载到内存中,则需要明确地将数据段的.bss部分清零。

1
其他答案非常完整和有帮助。实际上,在我的特定情况下,我只需要知道静态变量初始化为0时被放在.bss而不是.data中。将.bss部分添加到链接器脚本中,将一个清零的内存段放入映像中,解决了这个问题。

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