静态C变量未初始化

时间:2017-05-11 17:55:01

标签: gcc static linker x86 initialization

我有一个未初始化的文件级静态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';

一切都按预期工作:我可以清楚地看到变量在结果图像文件中的位置(即,我可以在图像文件的hexdump中看到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"

4 个答案:

答案 0 :(得分:3)

编译器可以肯定地符合他们自己的部分名称,但是使用我们从特定编译器知道的常见.data,.text,.rodata,.bss,这应该落在.bss中。

但这并不会自动将其归零。需要有一种机制,有时取决于你的工具链工具链处理它并创建一个二进制文件,除了.data之外,.rodata(和自然.text)被填入将填充二进制文件中的.bss。但取决于一些事情,主要是这是一个简单的ram only image,是一切生活在链接器脚本中的一个内存空间定义下。 例如,您可以在链接器脚本中将.dss放在.bss之后,并根据您使用的二进制格式和/或转换的工具,最终可能会在二进制文件中使用零内存,而无需任何其他工作。

通常情况下,您应该期望使用特定的工具链(链接器脚本是特定于链接器,而不是假定所有工具都是通用的)机制,以便从您的角度定义.bss的位置,然后是链接器的某种形式的通信。在它开始的地方和大小的大小,引导程序使用该信息,在这种情况下它的作用是零,并且可以假设它始终是引导程序的作业为零.bss自然有一些例外。同样,如果二进制文件是在只读媒体(rom,flash等),但.data和.bss是读/写,你需要在这个媒体上完整的.data然后有人必须复制它它在ram中的运行时位置,而.bss是其中的一部分,具体取决于工具链以及你如何使用它,或者起始地址和大小是在只读媒体上,有人必须在某个点之前将该空间归零() 。这又是引导程序的工作。设置堆栈指针,如果需要移动.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__;
}

然后你可以将它们拉入汇编语言bootstrap

.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__

然后编写一些代码来根据您的设计进行操作。

你可以简单地将这样的东西放在一个链接的汇编语言文件中而不用其他代码使用它们并汇编,编译其他代码和链接然后你喜欢的反汇编或其他工具会告诉你链接器生成了什么,调整直到你很满意然后你可以写或借或窃取bootstrap代码来使用它们。

对于裸机我更喜欢不完全符合我的代码标准,没有任何.data并且不希望.bss为零,所以我的引导程序设置堆栈指针并调用main,done。对于操作系统,您应该遵守。工具链已经为原生平台解决了这个问题,但是如果你用自己的链接器脚本和boostrap来接管它,那么你需要处理它,如果你想为现有的操作系​​统使用现有的工具链解决方案那么.. .done ...就这样做。

答案 1 :(得分:2)

这个答案只是其他人的延伸。如前所述,C standard有关于初始化的规则:

  

10)如果没有显式初始化具有自动存储持续时间的对象,则其值是不确定的。如果没有显式初始化具有静态存储持续时间的对象,则:

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

代码中的问题是计算机内存可能并不总是初始化为零。您可以确保 BSS 部分在独立环境(例如您的操作系统和引导程序)中初始化为零。

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

我知道您正在编写一个用于使用旧版BIOS进行x86启动的操作系统。我知道您正在使用其他近期问题中的 GCC 。我知道你正在使用GNU汇编程序作为引导加载程序的一部分。我知道你有一个链接器脚本,但我不知道它是什么样的。执行此操作的常用机制是通过链接器脚本将 BSS 数据放在末尾,并创建开始和结束符号以定义节的地址范围。一旦这些符号由链接器定义,它们就可以被 C 代码(或汇编代码)用于循环遍历该区域并将其设置为零。

我提出了一个相当简单的MCVE来做到这一点。代码读取带有Int 13h/AH=2h内核的额外扇区;启用A20线(使用快速A20方法);使用32位描述符加载 GDT ;启用保护模式;完成向32位保护模式的转换;然后在名为kmain C 中调用内核入口点。 kmain调用名为zero_bss C 函数,该函数根据起始符号和结束符号__bss_start和{{初始化 BSS 部分1}})由自定义链接描述文件生成。

__bss_end

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);
    }
}

答案 2 :(得分:1)

C标准规定y: 6 x: 2变量必须零初始化,即使没有显式初始值设定项,因此static等同于static char vgat_initialized= '\0';

在ELF和其他类似格式中,零初始化数据(例如此static char vgat_initialized;)转到.bss section.如果您自己将这样的可执行文件加载到内存中,则需要显式清零{{ 1}}数据段的一部分。

答案 3 :(得分:1)

其他答案非常完整,非常有帮助。事实证明,在我的具体情况下,我只需要知道初始化为0的静态变量放在.bss而不是.data。将.bss部分添加到链接描述文件中会在映像中放置一个已清零的内存部分来解决问题。