顯示具有 Embedded System 標籤的文章。 顯示所有文章
顯示具有 Embedded System 標籤的文章。 顯示所有文章

2016年10月19日 星期三

Rust 開發迷你ARM kernel 系列 0:Hello world

故事是這樣的,很久以前曾經在rust 上面實作hello world 的arm 程式,不過那時候的作法現在已經不能用,而且除了輸出x 之外其實不能幹嘛,更別提後面更多的東西了。

其實網路上也查得到不少Rust OS 的實作,沒道理我做不到,於是就來試一試了。

這裡不會說明程式碼為什麼要這樣寫,請參考之前寫的筆記:
https://yodaleeembedded2015.hackpad.com/Lab42-Note-YKuTRvCMYYx
mini-arm-os 的程式碼:
https://github.com/jserv/mini-arm-os
或者參考金門大學傳說中的鍾誠教授的<用十分鐘 向jserv學習作業系統設計>

--

要跑這個首先要安裝stm32 的qemu
https://github.com/beckus/qemu_stm32
注要在configure 的時候加上—disable-werror才能成功編譯,不然會遇到deprecated 的warning,完整的編譯參數
./configure --enable-debug --target-list="arm-softmmu" --python=/usr/bin/python2.7 --disable-werror

另外要將rust 編譯為arm,我們需要安裝rust 的cross compile tools,這裡有一篇文章把相關會遇到的問題都講得差不多了。
https://github.com/japaric/rust-cross

就算是一般使用我也推薦使用rustup,可以快速的在stable, beta, nightly 中切換;用rustup 處理cross compile 的問題也很容易,如上頁所述的五個步驟:
1. 安裝對應的C toolchain
2. 用rustup 安裝編譯好的目標library
3. 在~/.cargo/config指定特定target 的linker 要用誰,我猜這是因為LLVM 的linker 尚未就諸的緣故?這樣就可以用cargo build –target=$(target) 來指定編譯目標了;為了這個測試,我選用armv7-unknown-linux-gnueabihf,gcc 則是選用arm-linux-gnueabihf-gcc
rustup target add armv7-unknown-linux-gnueabihf
cat >> ~/.cargo/config < EOF
> [target.armv7-unknown-linux-gnueabihf]
> linker = "arm-linux-gnueabihf-gcc"
> EOF
cargo build --target=armv7-unknown-linux-gnueabihf
第一步是實作Hello world,雖然網路上有些純Rust 的實作,但這次想要自己重頭自幹,試圖完全用rust 代替c ,一些dirty work 總是少不了的,在最底層的部分還是先用 assembly 實作,找到適合的方法再用rust 改寫。

Assembly 的部分參考(複製貼上)這裡的code
https://community.arm.com/docs/DOC-8769

先寫一個最簡單的startup.S,isr 的部分只定義reset handler,並且用它的FUNCTION, ENDFUNC macro 實作defaultResetHandler和defaultExceptionHandler,內容物都是單純的迴圈:
.weakref            Reset_Handler,defaultResetHandler

.section            isr_vector
.align              2
.long               0  //initial stack pointer
.long               Reset_Handler // startup-code,系統上電時第一個執行的位址

.text
.align
FUNCTION            defaultResetHandler
b                   defaultExceptionHandler
ENDFUNC             defaultResetHandler

FUNCTION            defaultExceptionHandler
wfi                 // wait for an interrupt, in order to save power
b                   defaultExceptionHandler // loop
ENDFUNC             defaultExceptionHandler
編譯則是採用arm-none-eabi-gcc,參數使用 -fno-common -O0 -mcpu=cortex-m3 -mthumb -T hello.ld -nostartfiles,直接編譯就會動了,程式碼的hash 為 fdc836

當然只有assembly 是不夠的,我們要rust! 這裡參考之前看到這個神blog,它在x86 上面用asm 跟rust 自幹了一個頗完整的kernel,現在我的狀況跟他在接上rust 的地方有 87 % 像
http://os.phil-opp.com/set-up-rust.html

首先是寫一個Cargo.toml
[package]
name = "mini_arm"
version = "0.1.0"
authors = ["yodalee <lc85301@gmail.com>"]

[lib]
crate-type = ["staticlib"]

然後新建檔案 src/lib.rs
#![no_std]
#![feature(lang_items)]

#[lang = "eh_personality"]
extern fn eh_personality() {}
#[lang = "panic_fmt"]
extern fn panic_fmt() -> ! {loop{}}

#[no_mangle]
pub unsafe fn __aeabi_unwind_cpp_pr0() -> () { loop {} }

#[no_mangle]
pub extern fn rust_main() {
    loop {}
}
開頭先用#! 指定這個crate 的特性;指定 no_std免得rust std 那堆需要OS支援的檔案、system call 等東西跑進來亂;指定lang_items feature 讓我們可以去調整rustc 的一些行為,官方文件是這麼說的:
https://doc.rust-lang.org/book/lang-items.html
The rustc compiler has certain pluggable operations, that is, functionality that isn't hard-coded into the language, but is implemented in libraries, with a special marker to tell the compiler it exists.

大意是需要透過lang marker 來告訴rustc,這裡我們有實作(或說更改)了某項功能,例如下面的 eh_personality 跟panic_fmt;把feature 拿掉,我們實作 eh_personality會造成錯誤 language items are subject to change;把eh_personality 實作拿掉,則會變成language item required, but not found;有點…詭異。

eh_personality負責的是Rust在panic 時 unwinding 的工作,目前OS還不會unwinding 所以留白沒差;panic_fmt 則是panic! 的進入點,同樣不需要實作。
https://doc.rust-lang.org/nomicon/unwinding.html
__aeabi_unwind_cpp_pr0也是類似的狀況,如果不寫直接編譯,會發生undefined reference的錯誤,要使用 #[no_mangle] 避免function 名字被改掉;最後就是我們的main function,同樣要用no_mangle 來避免asm 找不到對應的function。

再來我們就能在reset handler 裡面動手腳了,把原本的迴圈改掉加上跳到rust_main 的指令:
FUNCTION            defaultResetHandler
bl rust_main
b  defaultExceptionHandler
ENDFUNC             defaultResetHandler
執行到這裡它就會進來執行我們的rust_main ;在Makefile 中加上cargo 的命令,就能成功編譯出執行檔,反編譯中也會看到對應的程式碼:
00000034 <rust_main>:

#[no_mangle]
pub extern fn rust_main() {
34: e24dd004  sub sp, sp, #4
  loop {}
38: eaffffff  b 3c <rust_main+0x8>
3c: eafffffe  b 3c <rust_main+0x8>

後面的內容就跟神blog 的內容講得差不多,需要在Cargo.toml 中加上rlibc的dependencies,並且在linker 參數加上 --gc-sections,才能使用一些rust 的code。
[dependencies]
rlibc = "1.0.0"

現在我們試著用qemu 執行時,qmeu 它爆炸了:
emu: fatal: Trying to execute code outside RAM or ROM at 0xe12fff1e

R00=00000000 R01=00000000 R02=00000000 R03=00000000
R04=00000000 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=ffffffe0 R14=fffffff9 R15=e12fff1e
PSR=40000153 -Z-- A svc32
FPSCR: 00000000

使用qemu 搭配gdb 來檢查一下:
qemu-system-arm -M stm32-p103 -nographic -kernel hello.bin -S -gdb tcp::9453
$(gdb) file hello.elf
$(gdb) target remote localhost:9453

它一進到rust_main 之後就死機了,當下的第一個指令是:
34: e24dd004 sub sp, sp, #4

很奇怪的,這行指令就是一直讓它當掉,比對了C version之後,發現可能是eabi 的問題:C用的是arm-none-eabi;我們則用了arm-linux-gnueabihf,於是我們要改用thumbv7em-none-eabi 的rustc;首先是從網路上拿到thumbv7em-none-eabi.json:
{
  "arch": "arm",
  "cpu": "cortex-m4",
  "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
  "disable-redzone": true,
  "executables": true,
  "llvm-target": "thumbv7em-none-eabi",
  "morestack": false,
  "os": "none",
  "relocation-model": "static",
  "target-endian": "little",
  "target-pointer-width": "32",
  "no-compiler-rt": true,
  "pre-link-args": [
    "-mcpu=cortex-m4", "-mthumb",
    "-Tlayout.ld"
  ],
  "post-link-args": [
    "-lm", "-lgcc", "-lnosys"
  ]
}
參考這篇裡面的作法: http://antoinealb.net/programming/2015/05/01/rust-on-arm-microcontroller.html
先把rust 的git repository 載下來,利用rust --version -v找到rustc 的build hash,將rust checkout 到相同的hash value
git clone https://github.com/rust-lang/rust
cd rust
git checkout $HASH
cd ..
把thumbv7em-none-eabi 存下來,就可以build 了:
mkdir libcore-thumbv7m
rustc -C opt-level=2 -Z no-landing-pads --target thumbv7em-none-eabi \
 -g rust/src/libcore/lib.rs --out-dir libcore-thumbv7em
先用rustc --print sysroot 找到rustc 的根目錄位置:
~/.multirust/toolchains/nightly-x86_64-unknown-linux-gnu
把編譯出的 libcore-thumbv7em/libcore.rlib,放到對應的資料夾中:$(rustc root dir)/lib/rustlib/thumbv7em-none-eabi/lib 裡面
$ pwd
~/.multirust/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib
$ tree thumbv7em-none-eabi
thumbv7em-none-eabi
└── lib
    ├── libcore.rlib
    └── rustlib
現在就可以用cargo build –target=thumbv7em-none-eabi 來編譯了;編譯完之後qemu 也不會當機了;同樣只有loop 的main,linux-eabi 跟none-eabi 的結果差異:
armv7-unknown-linux-gnueabihf, failed:
1c: e24dd004 sub sp, sp, #4
20: eaffffff b 3c <rust_main+0x8>

thumbv7em-none-eabi, worked:
10: b081 sub sp, #4
12: e7ff b.n 14 <rust_main+0x4>
到這裡我們就能來寫code 了,首先我們要把裡面的register 都獨立出來到一個reg.rs 裡面
#![allow(dead_code)]

/* RCC Memory Map */
pub const RCC: u32 = 0x40021000;
pub const RCC_APB2ENR: u32 = RCC + 0x18;
pub const RCC_APB1ENR: u32 = RCC + 0x1C;

/* GPIO Memory Map */
pub const GPIOA: u32 = 0x40010800;
pub const GPIOA_CRL: u32 = GPIOA + 0x00;
pub const GPIOA_CRH: u32 = GPIOA + 0x04;

/* USART2 Memory Map */
pub const USART2: u32 = 0x40004400;
pub const USART2_SR: u32 = USART2 + 0x00;
pub const USART2_DR: u32 = USART2 + 0x04;
pub const USART2_CR1: u32 = USART2 + 0x0C;
在main 裡面就能對各register 進行操作了,因為rust 對安全性的要求,所有對定位址的操作都是unsafe 的;另外之前支援的 number as *mut _ 已經不能用了,現在要指明哪一種型別的pointer:
https://doc.rust-lang.org/book/casting-between-types.html
const USART_FLAG_TXE: u16 = 0x0080;

pub fn puts(s: &str) {
    for c in s.chars() {
        unsafe {
            while !(*(reg::USART2_SR as *mut u32) & USART_FLAG_TXE as u32 != 0) {}
            *(reg::USART2_DR as *mut u32) = c as u32;
        }
    }
}

#[no_mangle]
pub extern fn rust_main() {
    unsafe { *(reg::RCC_APB2ENR as *mut u32) |= 0x00000001 | 0x00000004 };
    unsafe { *(reg::RCC_APB1ENR as *mut u32) |= 0x00020000 };
    unsafe { *(reg::GPIOA_CRL as *mut u32) = 0x00004b00 };
    unsafe { *(reg::GPIOA_CRH as *mut u32) = 0x44444444 };
    unsafe { *(reg::USART2_CR1 as *mut u32) = 0x0000000C };
    unsafe { *(reg::USART2_CR1 as *mut u32) |= 0x2000 };

    puts("Hello World!\n");
}

終於,我們看到傳說中的 Hello World! 啦,為了這一步可是歷經千辛萬苦呀

程式碼在此:
https://github.com/yodalee/rust-mini-arm-os/tree/master/00-Helloworld
請先進指教。

下一步:
我們依 mini-arm-os 的步調,下一步要加上更多的 register,我們要想辦法儘量不要增加編寫的負擔,能快速開發針對不同平台register 位址。

2016年5月13日 星期五

在X86機器上debug ARM 執行檔

以後有可能會用到,寫這篇純粹做個記錄。

故事是這樣子的,最近閒來無事研究一下傳說中 jserv 大神的amacc,有些地方實在看不出程式執行至此時一些變數的值為何,這時我們就要用gdb 了
不過amacc 是用 arm-linux-gnueabihf-gcc 編出來的arm 執行檔,我們host gdb 是X86 在執行時就會報錯:可執行檔格式錯誤
如果用arm-linux-gnueabihf-gdb 呢:它會寫 Don't know how to run.

我們需要用到server-client 的架構,server 端在實體target 上可以用 gdbserver,這會需要對gdb-server, gdb 特別編譯;gdb configure target 為arm-linux-gnueabi,gdbserver configure host 為arm-linux-gnueabi。
如這篇所述:
https://sourceware.org/gdb/wiki/BuildingCrossGDBandGDBserver

我們可以用qemu 的 debug 來代替gdbserver:
首先執行qemu ,指定debug 的port 為9453:
qemu-arm -L /usr/arm-linux-gnueabihf -g 9453 amacc tests/shift.c

在另外一個終端機,打開arm-linux-gnueabihf-gdb,這是已經configure target為arm-linux-gnueabi 的gdb:
arm-linux-gnueabihf-gdb ./amacc tests/shift.c

在gdb 裡面連接remote target:
target remote localhost:9453

再來就能用c 開始跑了,happy debug。

相關參考:
http://kezeodsnx.pixnet.net/blog/post/31901130-gdbserver-remote-debug-%E6%B8%AC%E8%A9%A6

2015年5月6日 星期三

使用 rust 來寫極簡的嵌入式系統

最近看到一些有趣的東西:
https://github.com/JinShil/rust_arm_cortex-m_semihosted_hello_world
https://github.com/neykov/armboot

用rust 來寫嵌入式系統,感覺相當生猛,正好最近在上傳說中的jserv 大神的嵌入式系統,就想把嵌入式系統作業用到東西,用rust 實作出來,主要參考的內容包括上面的armboot,跟作業的mini-arm-os:
https://github.com/embedded2015/mini-arm-os
這裡不細講mini-arm-os的內容,自行參考當時寫的潦草的筆記:
https://yodaleeembedded2015.hackpad.com/Lab42-Note-YKuTRvCMYYx

本篇相關的原始碼請見:
https://github.com/yodalee/rust-mini-arm-os

跟armboot 類似,我用了libarm/stm32f4xx.rs(變體)跟zero/std_types這兩個lib,不使用rust std的lib:
mod zero {
  pub mod std_types;
  pub mod zero;
}
#[macro_use]
mod libarm {
  #[macro_use] pub mod stm32f1xx;
}

zero比較簡單,定義一些C 裡面用到的型態應該對應到rust 什麼型態,和rust 基本的trait 如Sized, Copy 等等,不寫的話rustc 會抱怨找不到這些trait;就像在rust_hello_world這個實作裡面,也是把這些trait 寫在main裡面,我猜這些內容和Rust 的基本設計有關,目前還不是很清楚。
libarm就比較複雜,這感覺像是作者自己寫的,針對的是stm32f4, arm-cortex m4 的硬體。理論上這裡應該是先研究stm32f4要怎麼初始化,不過我偷懶,先直接把部分的stm32f4 硬體位址修改成stm32f1 的,這樣舊的code 就可以直接搬過來XD。

Stm32f4xx.rs的內容,簡單來說就是大量的直接定義,例如裡面的RCCType,直接對應RCC register的位址,每個位址會是32 bits 的空間,可以和stm32的文件內容一一對應,下面是我針對STM32f1修改後的RCC內容:
http://www.st.com/web/en/resource/technical/document/reference_manual/CD00171190.pdf
pub struct RCCType {
  pub CR: uint32_t,
  pub CFGR: uint32_t,
  pub CIR: uint32_t,
  pub APB2RSTR: uint32_t,
  pub APB1RSTR: uint32_t,
  pub AHB1ENR: uint32_t,
  pub APB2ENR: uint32_t,
  pub APB1ENR: uint32_t,
  pub BDCR: uint32_t,
  pub CSR: uint32_t,
}

在最後面,它用函式宣告傳回對應的Type struct:
#[inline(always)]
pub fn RCC() -> &'static mut RCCType {
  unsafe {
    &mut *(RCC_BASE!() as *mut RCCType)
  }
}

而RCC_BASE!() 這個Macro,又會照上一篇提到的Macro_rule展開為:AHB1PERIPH_BASE!() + 0x3000u32
一路展開,最後得到一個32 bits integer,再轉型成RCCType 的mutable pointer,我做的修改就是把RCC, USART2, GPIO的位址換成STM32f1的。
在main 裡面就可以使用let rcc = RCC() 的方式,取得RCC的pointer,並用像C一樣的操作手法來操作對應的register位址。
例如要修改APB1ENR,啟動週邊的clock:
let rcc = RCC();
rcc.APB1ENR |= 0x00020000u32;

或是對usart2 的位址取值都沒問題:
let usart2 = USART2();
while(true) {
  while(usart2.SR & 0x0080u16 == 0) {}
  usart2.DR = 'x' as uint16_t;
}

這裡我們只輸出'x',這是因為要把text 印出來,我們需要對str 作iterate,而這個東西是定義在rust 的core lib 裡面,一般安裝後在/usr/lib/rustlib裡面只會有x86_64_unknown_linux_gnu,如果要跑arm要先自己編譯arm的lib,相關的內容可見:
http://spin.atomicobject.com/2015/02/20/rust-language-c-embedded/
這步比較麻煩先跳過,之後研究出來再另文介紹。

另一個要解決的問題則是isr_vector,這裡可以看到一種很謎樣的寫法,用link_section這個attribute,定義區段名為 .isr_vector,並設定為一個array,內含一個extern “c” fn(),如果要需要其他的ISR,則可以在後面寫更多的function,並把1改為需要的數量。
#[link_section=".isr_vector"]
pub static ISRVECTORS: [unsafe extern "C" fn(); 1] = [
  main,
];

linker script裡面,先保留一個LONG 的寬度指向初始化stack pointer,接著放isr_vector的reset handler,再放其他的.text,這樣裝置一上電就會執行main裡的內容。
.text :
{
  LONG(_stackStart); /* Initial stack pointer */
  KEEP(*(.isr_vector)) /* ISR vector entry point to main */
  *(.text)
  *(.text*)
} > FLASH

如果我們把最終執行檔反組譯,會看到其中的位址配置,0x0指向stack start,0x4 reset_handler指向位在0x08 的main。:
Disassembly of section .text:
00000000 <_ZN10ISRVECTORS20h538ad2a8e3805addk6aE-0x4>:
0: 10010000 .word 0x10010000

00000004 <_ZN10ISRVECTORS20h538ad2a8e3805addk6aE>:
4: 00000009 ....

00000008 <main>:

執行結果,會印出滿坑滿谷的 'x',我加一個條件讓它只print 100個: 

在編譯時,要向rustc 指定target,這裡有一大篇文章講相關的內容,之後分另一篇文出來介紹:
https://doc.rust-lang.org/rustc_back/target/
同樣這份rust code 裡面有用到大量的rust attribute,也就是function 前的#[attribute],這也可以另外寫一篇文……
啊感覺挖了自己一堆坑,要趕快填坑了OAO。

結語:

在這篇文我試著解釋如何用rust 撰寫嵌入式系統程式,結論當然是做得到的,但整體比C寫的原始碼複雜得多,也不像C這麼直覺,編輯libarm 也是非常麻煩的工作。
由於嵌入式系統的code 通常都不會複雜到哪裡去(唔…大概吧),發揮不出Rust的優勢,我認為比起來還是寫C會有效率得多。

2015年4月26日 星期日

Linker script 簡介

Linker script,就是給Linker 看的script。

Linker:
當然這樣是在講廢話,首先要先知道Linker 是什麼:在程式編譯成物件檔之後,會把所有的物件檔集合起來交給連結器(linker),Linker 會把裡面的符號位址解析出來,定下真正的位址之後,連結成可執行檔。
例如我們在一個簡單的C 程式裡,include 一個標頭檔並使用裡面的函數,或者用extern 宣告一個外部的變數,在編譯成標頭檔的時候,編譯器並不清楚最終函數和變數的真正位址,只會留下一個符號參照。
待我們把這些東西送進linker,linker就會把所有的標頭檔整理起來,把程式碼的部分整理起來、變數的部分整理起來,然後知道位址了就把位址都定上去,如果有任何無法解析的符號,就會丟出undefined reference error。

我們可以試試:
外部函數,在一個foo.h 裡宣告,並在foo.c 裡面定義:
int foo();

外部變數,在var.c 裡面定義
int var;

在main.c 裡面引用它們:
#include “foo.h”
extern int var;
int main(){
  var = 10000;
  foo();
  return 0;
}

開始編譯
gcc -c main.c
gcc -c foo.c

這樣我們就得到兩個物件檔 main.o跟foo.o,我們可以用objdump -x 把物件檔main.o的內容倒出來看看,其中有趣的就是這個:
SYMBOL TABLE:
0000000000000000 g F .text 000000000000002a main
0000000000000000 *UND* 0000000000000000 var
0000000000000000 *UND* 0000000000000000 foo RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000011 R_X86_64_PC32 var-0x0000000000000008
000000000000001f R_X86_64_PC32 foo-0x0000000000000004

可以看到var, foo 這兩個符號還是未定(UND, undefined),若我們此時強行連結,就會得到:
main.c:(.text+0x11): undefined reference to 'var'
main.c:(.text+0x1f): undefined reference to'foo'

必須把foo.o 跟var.o 兩個檔案一起連結才行。

--
Linker script:
好了Linker講了這麼多,那linker script 呢?

Linker script 可以讓我們對linker 下達指示,把程式、變數放在我們想要的地方,一般的gcc 都有內建的linker script,平常我們開發x86系統跟arm系統,會使用不同的gcc,就是在這些預設的設定上有所不同,要是把這團亂七八糟的東西每key一次gcc 都要重輸入就太麻煩了;可以用ld --verbose 輸出,這裡看到的是支援x86 系統的linker script ,講下去又另一段故事,先跳過不提。

我們這裡拿燒錄在STM32 硬體上的linker script 來講,linker script 可見:
https://github.com/yodalee/mini-arm-os/blob/master/02-ContextSwitch-1/os.ld

Linker 的作用,就是把輸入物件檔的section整理成到輸出檔的section,最簡單的linker script 就是用SECTIONS指令去定義section 的分佈:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}

在Linker script 裡面,最要緊的就是這個符號 '.' location counter,你可以想像這是一個探針,從最終執行檔的頭掃到尾,而 '.' 這個符號就指向現在掃到的位址,你可以讀取現在這個探針的位址,也可以移動探針。
不指定的話location counter 預設會從0的位置開始放置,而這段script,先把location counter 移到0x10000,在這裡寫入.text section,再來移到0x8000000放.data 跟.bss。
這裡檔名的match 支援適度的正規表示式,像*, ?, [a-z] 都可以使用,在這裡用wildcard直接對應到所有輸入檔案的sections。
光是SECTION 就講不清的用法,把指定某檔案的Section (file.o(.data)),排除某些檔案的section (EXCLUDE_FILE)
幸運的是,通常我們都不會想不開亂改linker script,這些位置的放法要看最終執行的硬體而定,亂放不會有什麼好下場。
另外linker script 也定義一些指令,這裡列一些比較常用的:

ENTRY:
另外我們可以用ENTRY指定程式進入點的符號,不設定的話linker會試圖用預設.text 的起始點,或者用位址0的地方;在x86 預設的linker script 倒是可以看到這個預設的程式進入點:
ENTRY(_start)

既然linker script 是用來解析所有符號的,那它裡面能不能有符號,當然可以,但有一點不同,一般在C 語言裡寫一個變數的話,它會在symbol table 裡面指明一個位址,指向一個記憶體空間,可以對該位址讀值或賦值;而在linker script 裡的符號,就只是將該符號加入symbol table內,指向一個位址,但位址裡沒有內容,定義這個符號就是要取位址。
一般在linker script 裡面定義符號,都是要對記憶體特定位址作操作:
以上面的STM32 硬體為例,因為FLASH 記憶體被map 到0x00000000,RAM的資料被指向0x20000000,為了把資料從FLASH 搬到RAM 裡,在linker script 的RAM 兩端,加上了:
_sidata = .;
//in FLASH _sdata = .;
_edata = .;

等於是把當前 location counter 這根探針指向的位址,放到_sdata 這個符號裡面,所以在主程式中,就能向這樣取用RAM 的位址:
extern uint32_t _sidata;
extern uint32_t _sdata;
extern uint32_t _edata;

uint32_t *idata_begin = &_sidata;
uint32_t *data_begin = &_sdata;
uint32_t *data_end = &_edata;
while (data_begin < data_end) *data_begin++ = *idata_begin++;

注意我們用reference 去取_sdata, _edata 的位址,這是正確用法。

Linker script 還定義了PROVIDE 指令,來避免linker script 的符號跟C中相衝突,上面如果在C程式裡有_sdata的變數,linker 會丟出雙重定義錯誤,但如果是
PROVIDE(_sdata = .)
就不會有這個問題。

KEEP 指令保留某個符號不要被最佳化掉,在script 裡面isr_vector是exception handler table,如果不指定的話它會被寫到其他區段,可是它必須放在0x0的地方,因此我們用KEEP 把它保留在0x0上。

MEMORY:
Linker 預設會取用全部的記憶體,我們可以用MEMORY指令指定記憶體大小,例子中我們指定了FLASH跟RAM的輸出位置與大小:
MEMORY { FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 40K

} 接著我們在上面的SECTION部分,就能用 > 符號把資料寫到指定的位置
也就是例子裡,把 .text section全塞進 FLASH位址的寫法,如果整體程式碼大於指定的記憶體,linker 也會回報錯誤。

結語:
Linker 其實是個古老而複雜的東西,Linker script 裡面甚至有OVERLAY這個指令,來處理overlay 的執行檔連結,但一般來說,除非是要寫嵌入式系統,需要對執行檔的擺放位置做特別處理,否則大部分的程式都不會去改linker script,都直接用預設的組態檔下去跑就好了。

這篇只介紹了極限基本的linker script,完整內容還是請看文件。

參考內容:
Linker script document:
https://sourceware.org/binutils/docs/ld/Scripts.html

如果要知道linker如何處理位置無關符號,請見:
https://www.technovelty.org/c/position-independent-code-and-x86-64-libraries.html

2015年4月13日 星期一

用llvm 編譯嵌入式程式

最近幾天在研究嵌入式系統,玩一玩也有一些心得。
課程上所用的編譯工具是arm-none-linux-gnu toolchain,在Archlinux 下可以用如下的方式安裝:
$ yaourt -S gcc-linaro-arm-linux-gnueabihf
$ yaourt -S qemu-linaro
$ yaourt -S arm-none-eabi-gcc49-linaro
$ yaourt -S arm-none-eabi-gdb-linaro
$ ln -s /opt/gcc-linaro-arm-linux-gnueabihf/libc /usr/arm-linux-gnueabihf

不過最近心血來潮,想來試試如果用另一套編譯器 LLVM 來編譯看看,至於為什麼…好玩嘛(炸),總之這裡是設定筆記:

主要參考網址:
http://clang.llvm.org/docs/CrossCompilation.html
https://github.com/dwelch67/mbed_samples/

用上LLVM 的優勢是,它在編譯時會將程式碼轉換成與平台無關的中間表示碼(Intermediate Reprsentation, IR),再透過轉換器轉成平台相關的組合語言或是底層的機械器。不像gcc 針對不同的Host/Target的組合就是不同的執行檔和標頭檔,在編譯到不同平台時,都要先取得針對該平台的gcc 版本。
註:上面這段是譯自上面的參考網址,雖然我有點懷疑這段話,不然gcc 命令列參數那些平台相關的選項是放好看的嗎?

我嘗試的對象是mini-arm-os 00-helloworld,目標device 是STM32
https://github.com/embedded2015/mini-arm-os

前端的 c 我們先用clang 編譯為llvm IR code,用llvm 的llc 編譯為 object file,因為目前LLVM 的linker lld還在開發中,只能link x86上的elf 檔,要連結ARM 我們在link 階段還是只能用biutils 的ld,以及biutils 的objcopy,這樣看起來有點詭異,有點像換褲子結果褲子只脫一半就穿新褲子的感覺。

最後的Makefile 大概長這樣:

CC := clang
ASM := llc
LINKER := arm-none-eabi-ld
OBJCOPY := arm-none-eabi-objcopy

CCFLAGS = -Wall -target armv7m-arm-none-eabi
LLCFLAGS = -filetype=obj
LINKERSCRIPT = hello.ld

TARGET = hello.bin
all: $(TARGET)

$(TARGET): hello.c startup.c
$(CC) $(CCFLAGS) -c hello.c -o hello.bc
$(CC) $(CCFLAGS) -c startup.c -o startup.bc
$(ASM) $(LLCFLAGS) hello.bc -o hello.o
$(ASM) $(LLCFLAGS) startup.bc -o startup.o

$(LINKER) -T hello.ld startup.o hello.o -o hello.elf
$(OBJCOPY) -Obinary hello.elf hello.bin

其實重點只有在target 指定的地方,其他的就沒啥,只是這樣一看好像沒有比較方便,而且這樣根本就不是用 llvm 編譯,最重要的Link 階段還不是被 gcc 做去了= =
在lld 完成前也許還是乖乖用 gcc 吧?

對LLVM 相關介紹可以見:
http://elinux.org/images/d/d2/Elc2011_lopes.pdf
各種biutils 的替代品列表:
http://marshall.calepin.co/binutils-replacements-for-llvm.html
LLVM lld開發中,是不是該進去貢獻一下Orz:
http://lld.llvm.org/