現在的 main 程式現在只能使用 stack variable,還不能使用 static 變數,因為我們在 linker 內只放了 .text 區段, static 變數 所用的 .data(已初始化)、.bss(未初始化) 都還沒準備。
.data .bss
讓我們補上這塊,首先是 .rodata .bss .data 區段,在 linker script 裡面加上這些區段:
.rodata :
{
*(.rodata .rodata.*);
} > FLASH
.bss :
{
*(.bss .bss.*);
} > RAM
.data :
{
*(.data .data.*);
} > RAM
用 rust 裡面很不受歡迎的 global 變數來生成 DATA BSS 區段的資料;
RODATA 區段可以用 static array 或像這樣用 const 來產生;在 main 裡面一定要 reference 它們,不然 linker 就會很高興的把它們移除掉。
另外在 rust 裡面對 static mut 取 reference 是 unsafe behavior,因為 static 有可能被多個報行緒共享。
const RODATA2: &[u8] = b"hello";
static mut BSS: u8 = 0;
static mut DATA: u32 = 0x5a5aa5a5;
#[no_mangle]
pub fn main() -> ! {
let _x = RODATA;
let _y = unsafe { &BSS };
let _z = unsafe { &DATA };
loop {}
}
編譯
編譯完之後,用 objdump 檢視編譯出來的執行檔,可以在裡面找到 .data 區段:
$ arm-none-eabi-objdump -s target/thumbv7m-none-eabi/debug/app -j .data
Contents of section .data:
20000004 a5a55a5a
編譯器並沒有輸出 .bss 區段的資料,或說 objdump 不會顯示 .bss 區段的資料(雖然我不知道為什麼),但是 .bss 其實是存在的, 上面的 linker script 指定 RAM 裡面依序放著 .bss 跟 .data 區段,因此 RAM 開始的 0x2000_0000 就放著 .bss, 0x2000_0004 則放著 0x5a5aa5a5 的 .data 區段。
LMA 與 VMA
讓我們把執行檔轉成要燒錄到硬體上的二進位檔:
$ arm-none-eabi-objcopy -O binary target/thumbv7m-none-eabi/debug/app bin
$ ls -l bin
513M 6月 22 12:09 bin
這個尺寸竟然高達 513 MB。
原因是我們指定了 .data 區段從 RAM 開始,而目標機器的 RAM 位址是 0x20000000 ,也就是 512MB 的地方開始,
放上 .bss 跟 .data 的資料後結束,於是 objcopy 就產生了一個 513MB 的二進位檔出來。
但這個二進位檔是沒意義的,燒錄的時候只有燒進 flash 裡面才有用,RAM 裡的在斷電時資料就消失了。
現下這段 code 在 qemu 能夠正確執行(可能因為 qemu 會拿著 binary 塞進虛擬的 RAM 裡面,並沒有模擬斷電資料未初始化的情形), 在實際的硬體上,讀取 static 變數會得到一堆亂數。 再次修改 linker script,在 bss data 區段開始跟結束提供符號,並且利用 AT 設定 .data 的 LMA :
.bss :
{
_sbss = .;
*(.bss .bss.*);
_ebss = .;
} > RAM
.data AT(ADDR(.rodata) + sizeof(.rodata)):
{
_sdata = .;
*(.data .data.*);
_edata = .;
} > RAM
_sidata = LOADADDR(.data);
修改如下:
- _sbss, _ebss, _sdata, _edata 符號標示 bss 跟 data 的開始與結束
- .data 用 AT 設定 LMA,排在 .rodata 之後
- _sidata 符號使用 LOADADDR 取得 .data 的 LMA,_sdata, _edata 取得的則是 VMA。
編譯後,這次用 objcopy 產生的燒錄檔的尺寸就正常多了,只需要 161 bytes。
用 objdump -h
也能看到 .data 的 LMA 與 VMA 確實被分開了:
$ arm-none-eabi-objdump -h target/thumbv7m-none-eabi/debug/app
Sections:
Idx Name Size VMA LMA File off Algn
...
4 .data 00000004 20000004 0000009d 00020004 2**2
CONTENTS, ALLOC, LOAD, DATA
在我們 .rodata (Hello World 之後) 也能看到 0x5a5aa5a5 的 .data 區段,也就是說 .data 的初始資料已經包含到二進位燒錄檔內, 在燒錄進 flash 的時候,就會把這個初始值跟著燒進 flash。
$ arm-none-eabi-objcopy -O binary target/thumbv7m-none-eabi/debug/app bin
$ xxd bin
00000080: fede ffe7 fee7 0000 4865 6c6c 6f00 0000 ........Hello...
00000090: 8800 0000 0500 0000 576f 726c 64a5 a55a ........World..Z
000000a0: 5a Z
光這樣是不夠的,我們還需要在程式啟動,進到 main 之前,把這些值得 flash 複製到 RAM 裡面才行。
在 reset_handler 呼叫 main 之前加入這段 code,使用 core::ptr
的 write_bytes 初始化 bss 區段為 0 ,
用 copy_nonoverlapping 將資料從 flash 複製到 RAM 裡面:
extern "C" {
static mut _sbss: u8;
static mut _ebss: u8;
static mut _sdata: u8;
static mut _edata: u8;
static _sidata: u8;
}
let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize;
ptr::write_bytes(&mut _sbss as *mut u8, 0, count);
let count = &_edata as *const u8 as usize - &_sdata as *const u8 as usize;
ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, count);
// call main
這樣程式在硬體上就能正常運作了。
其他
我注意到在 RODATA,要產生 RODATA 有下列兩種寫法:
static STATIC_RO: &[u8] = "Hello";
const CONST_RO: &[u8] = "World";
兩種寫法會產生兩種不同的 .rodata: 第一種寫法,會產生三個部分:資料本體、u32 資料起始位址、u32 資料長度。 第二種寫法,只會產生資料本體。
以我上面為例,編譯出來的 .rodata 會是:
$ arm-none-eabi-objdump -s target/thumbv7m-none-eabi/debug/app -j .rodata
Contents of section .rodata:
0088 48656c6c 6f000000 88000000 05000000 Hello...........
0098 576f726c 64 World
Hello
之後 0x88 為起始位址,0x5 是長度;World
則是以單純的資料存在。
我是不清楚 static 跟 const 在 rust 裡面最後怎麼轉成物件檔的資料區段,看起來用 const 比用 static 還要節省一咪咪的空間,
但為什麼會有這個差異我就不懂了,希望有了解的大大可以為小弟指點一下迷津。