為什麼使用gcc編譯代碼後局部數組變數的初始值消失了?

簡要版: 在指定下面的編譯選項後, 如果我在函數中(包括主函數)使用

unsigned char a[] = {"12345"}; 後, 實際上生成的a數組確是空的, "12345"這個串在彙編文件和最後的目標文件以致內核文件都找不到, 而如果我用的是全局變數, 則一切正常.

2014.11.18 首先非常感謝回答我的知友們,現在補充一些詳細一點的信息,

全部的代碼我放在LastAvenger/OS67 · GitHub 上。

也貼上了-S參數生成的代碼。

詳細版:

說明一下背景, 題主正在做一個玩具的x86內核, 所以和平時的編譯有所不同, 使用的編譯選項是:

CFLAGS = -Wall -Werror -nostdinc -fno-builtin -fno-stack-protector
-finline-functions -finline-small-functions -findirect-inlining
-finline-functions-called-once -I./kern

鏈接腳本: (事實上我對這個不太了解...)

ENTRY(start)
SECTIONS
{
.text 0x8000: {
*(.text)
}
.data : {
*(.rodata);
*(.data);
}
.bss : {
*(.bss)
}
}

如果我在內核的入口函數中這樣寫:

#include &
int osmain(void)
{
init_vga();
unsigned char a[] = {"12345
"};
puts(a);
for (;;);
}

gas彙編代碼是:

.file "main.c"
.text
.globl osmain
.type osmain, @function
osmain:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $40, %esp
call init_vga
movl $875770417, -15(%ebp)
movw $2613, -11(%ebp)
movb $0, -9(%ebp)
leal -15(%ebp), %eax
movl %eax, (%esp)
call puts
.L2:
jmp .L2
.cfi_endproc
.LFE0:
.size osmain, .-osmain
.ident "GCC: (SUSE Linux) 4.8.1 20130909 [gcc-4_8-branch revision 202388]"
.section .note.GNU-stack,"",@progbits

程序不會輸出任何東西, 也可以看到彙編代碼中根本沒有「12345」之類的串:

(回復@鍾宇騰 :即使是去了花括弧或者加const也沒有用 );

(回復@朱小餓:加了volatile無效, 我今天才發現有這個關鍵字)。

如果我這麼寫:

#include &
int osmain(void)
{
init_vga();
puts((unsigned char *)"12345");
for (;;);
}

彙編代碼如下:

.file "main.c"
.section .rodata
.LC0:
.string "12345"
.text
.globl osmain
.type osmain, @function
osmain:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
call init_vga
movl $.LC0, (%esp)
call puts
.L2:
jmp .L2
.cfi_endproc
.LFE0:
.size osmain, .-osmain
.ident "GCC: (SUSE Linux) 4.8.1 20130909 [gcc-4_8-branch revision 202388]"
.section .note.GNU-stack,"",@progbits

可以看到多了一個.rodata 段, 很明顯的, 「12345」作為一個常量出現在這個段里。

結果則是這樣:

如果我寫成:

#include &
unsigned char a[] = {"12345
"};
int osmain(void)
{
init_vga();
puts(a);
for (;;);
}

.file "main.c"
.globl a
.data
.type a, @object
.size a, 7
a:
.string "12345
"
.text
.globl osmain
.type osmain, @function
osmain:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
call init_vga
movl $a, (%esp)
call puts
.L2:
jmp .L2
.cfi_endproc
.LFE0:
.size osmain, .-osmain
.ident "GCC: (SUSE Linux) 4.8.1 20130909 [gcc-4_8-branch revision 202388]"
.section .note.GNU-stack,"",@progbits

和上面的結果一樣, 不同的是文件裡面a是一個獨立的label...

那麼問題來了,如何解釋上面的這幾種情況?按我粗淺的想法看,只要是定長的數組(即不要使用堆)的話,程序就應該能正常運行才對,即使以上的問題不解決,內核也可以繼續寫下去,但是就不能使用局部數組。。。實在覺得不爽。。。

(回復: @hang dong

puts,putchar代碼如下,vgamem自然是顯存,用了一個結構數組來描述它(使用網上那種移位的方法發生錯誤),更多代碼還煩請移步到我的GitHub。

void putchar(unsigned char ch){
switch (ch){
case "
": cur_x = 0; break;
case "
": cur_y++; break;
default: {
vgamem[cur_y*80 + cur_x]._char = ch;
vgamem[cur_y*80 + cur_x].f_color = COL_L_GREY;
vgamem[cur_y*80 + cur_x].b_color = COL_BLACK;
cur_y += (cur_x + 1)/80;
cur_x = (cur_x + 1)%80;
}
}
}

void puts(unsigned char *str){
int i = 0;
for (i = 0; i &< 6; i ++) putchar(str[i]); }

(另外還有一個問題, 使用相同的工具(Linux 下的 gcc + nasm + ld + objcpy 和 Windows 下的MinGW), 前者生成的內核只有幾百B, 而後者卻達到了驚人的12K左右...這又是為什麼呢?


簡要版

這個問題和編譯器無關,是 CPU 的分段配置有問題。

詳細版

//先貼反彙編吧。

//

//另外,段寄存器 ss 和 ds 是一樣的么?對應的 GDT 中的基址相同么?

一、"12345
" 在哪

在這段代碼

#include &
int osmain(void)
{
init_vga();
unsigned char a[] = {"12345
"};
puts(a);
for (;;);
}

的彙編代碼中,

movl $875770417, -15(%ebp)
movw $2613, -11(%ebp)
movb $0, -9(%ebp)

這三句話把 a 數組初始化成了 "12345
" 。

易知 a 的地址是 ss:(%ebp-15)。

leal -15(%ebp), %eax
movl %eax, (%esp)
call puts

這三句話將 a 的地址作為參數傳給 puts。

二、為什麼沒有輸出

這裡有個問題。

當沒有明確指定定址的段時,寄存器有默認的定址的段。

%ebp 默認是在 ss 段上定址,多數通用寄存器是在 ds 段上定址。

而且傳參給 puts 後會在 ds 上定址。

也就是說,puts 訪問的是錯誤的地址 ds:(%ebp-15) 而不是數組 a 的實際地址 ss:(%ebp-15)。

這兩個地址不同的原因如下:

//你在評論中說 ds 的基址是 0,ss 基址是 0x7c00。

你的 GitHub 上的 GDT 表如下:

(OS67/bootsect.asm at bded6026ed3c6ea77eba650ae261ba3f9ee332f4 · LastAvenger/OS67 · GitHub)

GDT:
DESC_NULL: Descriptor 0, 0, 0 ; null
DESC_CODE32_R0: Descriptor 0x8000, 0xffff, DA_C+DA_32 ; uncomfirm
DESC_DATA_R0: Descriptor 0, 0xffffffff, DA_DRW+DA_32 ; 4G seg
DESC_VIDEO_R0: Descriptor 0xb8000, 0xffff, DA_DRW+DA_32 ; video RAM DA_32
DESC_STACK_R0: Descriptor 0x7c00, 0xfff, DA_DRWA+DA_32

此時 ds:(%ebp-15) != ds:(%ebp-15 + 0x7c00) == ss:(%ebp-15)。

三、一個測試

#include &
int osmain(void)
{
init_vga();
unsigned char a[] = {"12345
"};
puts(a + 0x7c00);
for (;;);
}

這段代碼應該是能正常輸出的。

四、如何解決

很簡單,把 ss 基址設計成和 ds 一樣就行了。

五、其他

你的 cs 段的基址是 0x8000,而不是 0。鏈接腳本也沒有相關處理,代碼中似乎也沒在運行時移動。

如果你使用絕對跳轉,比如函數指針,會出問題的。

解決方法是把 cs 基址設成 0

//,或在鏈接腳步中指定合適的 .text 段的 VMA 和 LMA (Output Section LMA)

//,或其他可行的方式


同意 @郭家華 的判斷。

另外,我告訴題主一個比較容易的判斷方法:

編譯腳本里,加上一個-g的參數,會生成調試符號,調試符號里是帶行號的,如下圖:

然後你就知道具體哪一行在幹什麼事情了,之後反推一下是很容易的。


局部變數在棧上 不在數據段 運行時初始化的


movl $875770417, -15(%ebp)
movw $2613, -11(%ebp)

875770417不就是是"1234"么,2613不就是"5
"么


你puts()咋寫的?

inline+不正確編寫的函數可能造成編譯器錯誤的優化假設.把你的東西優化掉.


推薦閱讀:

怎麼來學習c++?
學習完 C++ Primer 能做什麼項目練手或者看什麼好的開源項目源碼?
關於C++宏定義的一個疑問?
我們用的計算機語言底層都是用什麼寫的?
上溢後,結果為什麼可以用 (原值%對應數據類型最大值) 求出?

TAG:中央處理器CPU | C編程語言 | CC | 編譯器 | 操作系統內核 |