Rust no-stdのasync完全理解を目指そう!
はじめに
この記事はRust Advent Calendar 2019の17日目として書きました。
組込みRust界の神japaric
さんがno-std
環境でasync
を使うPoCレポジトリを公開しています。
理解できるかどうか非常に自信がありませんが、これは見てみるしかありません!
後日正式な記事が書かれるそうなので、それを待ったほうが得策かもしれません!
引用の領域超えている気がしますので、一応ライセンス表記します。 今回解説するレポジトリは、MIT license、もしくは、Apache License, Version 2.0、でライセンスされています。
目次
自分なりのまとめ
- 組込みの
no-std
環境で使えるasync-await
のproof of conceptを紹介するよ (nightlyは必要だけどね!) - cooperativeスケジューラ (executor) は割り込みハンドラから完全に隔離するよ
- なので割り込みハンドラで実行されるリアルタイム性が要求されるコードの予測性を損なわずに、cooperativeなコードを実行できるよ
- そのために、
executor
は#[global_allocator]
とは違う専用のメモリアロケータを使うよ - 現在Rustのcollectionは
global_allocator
を使うようにハードコーディングされているので、collectionを書き直す必要があるよ
つまるところ、async-await
を使うと、リアルタイムタスクの最悪実行時間が計算しずらくなるため、割り込みコンテキストと完全に分離できるように、async-await
をno-std
環境で使えるようにした、という話のようです。
現在のところ、no-std
環境はCortex-M
に限定されています。
実装も覗いてみましたが、手続きマクロ以外は割と読めそうな感じです。
ツール
nightlyツールチェインと、Cortex-M3向けのクロスビルド環境が必要です。
$ rustup override set nightly $ rustup update $ rustup target add thumbv7-none-eabi
後、実行環境として、qemu-system-armを使います。
$ cargo run --example async-await --features="nightly"
README
まずなにはともあれ、プロジェクトのREADMEを見てみましょう。 と思ったらREADMEだけで1100行あるではありませんか! これは前途多難な予感です…。
Goal
Real Time For the Masses (RTFM) と一緒に使えるリアルタイムアプリケーション向けのcooperativeスケジューラを作ることが目標のようです。 cooperativeスケジューラは最悪実行時間の解析が難しくなります。
Background
Asynchronous code
皆さんご存知のようにRust 1.39 からasync
/ await
の機能が安定化しました。
非同期コードはexecutor
によって実行されることを意味します。
executor
は標準ライブラリでは提供されていませんが、async-std
やtokio
といったマルチスレッドexecutor crateがあります。
async fn
のインスタンスはtask
になり、executorがスケジューリング、実行します。
// toolchain: 1.39.0 // async-std = "1.2.0" use async_std::task; fn main() { // schedule one instance of task `foo` -- nothing is printed at this point task::spawn(foo()); println!("start"); // start task `bar` and drive it to completion // this puts the executor to work // it's implementation defined whether `foo` or `bar` runs first or // whether `foo` gets to run at all task::block_on(bar()); } async fn foo() { println!("foo"); } async fn bar() { println!("bar"); }
$ cargo run start foo bar
サンプルコード内のコメントによると、タスクの実行順序はexecutorの実装依存なのですね。 勉強になります!
executorはタスクを協調的に実行します。
最も単純な場合、.await
に到達するまでタスクを実行し、実行をブロックする必要がある場合はそのタスクをサスペンドし、別のタスクをresume
します。
Some implementation details
シンタックス的には、ジェネレータは、サスペンションポイント (yield
) を含む、クロージャのような (|| { .. }
) ものです。
// toolchain: nightly-2019-12-02 use core::{pin::Pin, ops::Generator}; fn main() { let mut g = || { println!("A"); yield; println!("B"); yield; println!("C"); }; let mut state = Pin::new(&mut g).resume(); println!("{:?}", state); state = Pin::new(&mut g).resume(); println!("{:?}", state); state = Pin::new(&mut g).resume(); println!("{:?}", state); }
セマンティクス的には、ジェネレータはyield
間の状態マシンで、外部からresume
されることで状態遷移します。
ふむふむ。
executor
は状態マシンのリストを持っていて、全ての状態マシンが完了になるまで、resume
し続けます。
各状態マシンは異なるサイズで違うコードを実行するので、トレイトオブジェクト (Box<dyn Generator>
) としてリストされます。
これはサンプルコードを見ると理解しやすいです。
fn executor(mut tasks: Vec<Pin<Box<dyn Generator<Yield = (), Return = ()>>>>) { let mut n = tasks.len(); while n != 0 { for i in (0..n).rev() { let state = tasks[i].as_mut().resume(); if let GeneratorState::Complete(()) = state { tasks.swap_remove(i); // done; remove } } n = tasks.len(); } }
なるほど。タスク (状態マシン) をVec<Box<dyn Generator>>
として受け取り、各タスクのresume
を呼ぶ。
状態が完了
になると、Vec
からswap_remove
する、と。
Idea
アプローチとしては、非同期コードを#[idle]
もしくはfn main
に隔離します。
その理由は
- 協調的タスクには終了しないものと、短期間で終了するもがあるため、終了しない
#[idle]
タスクでexecutor
を動かすのが賢明である executor
で必要となる動的メモリ確保は、#[idle]
に制限されます。#[idle]
内ではリアルタイムでないアロケータを使用し、通常のタスクは動的メモリ確保をしないようにします。アロケータを#[idle]
内で排他的に使用することで、mutex
などの排他制御が不要になります。
ふむ、よくわからないので、もう少し先を見てみましょう。
Implementation
実装には2つのコンポーネントがあります。「スレッドモード」アロケータと「スレッドモード」executorです。 「スレッドモード」はARMの「スレッドモード」を意味しています。
アロケータとexecutorは、「スレッドモード」でのみ利用できます。 ARMの「ハンドラモード」ではアロケータとexecutorにアクセスできません。
「ハンドラモード」は主に割り込みや例外を処理するためのモードです。
Cortex-Mで、リセットハンドラは「スレッドモード」で実行されます。
RTFM
アプリでは、#[init]
と#[idle]
は「スレッドモード」で実行します。
TM (Thread-Mode) allocator
TMアロケータはseparate allocator
です。ん?どういうことでしょう?
#[global_allocator]
で定義されているものとは、独立アロケータです。ああ、そういうこと。
理想的には、RFC #1398で提案されているallocator-generic
コレクションをAlloc
トレイトを通じて使えると良いのですが、Alloc
トレイトは安定化していませんし、allocator-generic
コレクションは存在していません。
あー、なるほど。C++のようにカスタムアロケータが設定できるコレクションが提案されているのですね?(要確認)
今のコレクションは、#[global_allocator]
を使うようにハードコーディングされています。
TMアロケータはstableで実装できますが、コレクションはそうではないようです。 Rcで使用するcore::intrinsics::abort
がunstableであるなどの理由で。
stableではコレクションの型強制もできないようです。Box<impl Generator>
は使えないため、Box<dyn Generator>
を使います。
// toolchain: 1.39.0 use cortex_m_tm_alloc::allocator; use tlsf::Tlsf; #[allocator(lazy)] static mut A: Tlsf = { // `MEMORY` is transformed into `&'static mut [u8; 64]` static mut MEMORY: [u8; 64] = [0; 64]; let mut tlsf = Tlsf::new(); tlsf.extend(MEMORY); tlsf };
TMアロケータをA
という名前で定義します。A
は実行時に[TLSF]アロケータを初期化します。
A
アロケータのハンドラはget
コンストラクタで取得します。
get
コンストラクタは、Option<A>
を返します。「スレッドモード」で呼び出すとSome
ヴァリアントが、「ハンドラモード」で呼び出すとNone
が帰ります。
A
は、Copy
とAlloc
トレイトを実装したサイズ0の型です。Send
とSync
トレイトは実装していないため、インスタンスを割り込み / 例外ハンドラに渡すことができません。
な、なるほど…。
#[entry] fn main() -> ! { hprintln!("before A::get()").ok(); SCB::set_pendsv(); if let Some(a) = A::get() { hprintln!("after A::get()").ok(); SCB::set_pendsv(); // .. } else { // UNREACHABLE } // .. } #[exception] fn PendSV() { hprintln!("PendSV({:?})", A::get()).ok(); }
$ cargo run before A::get() PendSV(None) after A::get() PendSV(None)
PendSV
ハンドラ内では、A::get()
してもNone
になっています。
#[entry]
内では、Some
ヴァリアントが得られていますね(2回めのPendSVに突入していることから)。
if let Some(a) = A::get() { // .. let mut xs: Vec<i32, A> = Vec::new(a); for i in 0.. { xs.push(i); hprintln!("{:?}", xs).ok(); } }
一度アロケータインスタンスを取得すれば、コレクションの初期化時にアロケータのコピーを渡すことで、アロケータを使用できます。 アロケータはサイズ0の型なので、スタックサイズは増えません。
if let Some(a) = A::get() { // .. let mut xs: Vec<i32, A> = Vec::new(a); for i in 0.. { xs.push(i); hprintln!("{:?}", xs).ok(); } }
グローバルアロケータと同様に、TMアロケータもOut Of Memoryになる可能性があります。
その場合、#[oom]
アトリビュートを使って定義されたOut Of Memoryハンドラが呼ばれます。
#[alloc_oom::oom] fn oom(layout: Layout) -> ! { hprintln!("oom({:?})", layout).ok(); debug::exit(debug::EXIT_FAILURE); loop {} }
$ cargo run [0] [0, 1] [0, 1, 2] [0, 1, 2, 3] oom(Layout { size_: 32, align_: 4 }) $ echo $? 1
TM (Thread-Mode) executor
TM executorはTMアロケータに依存しています。
// toolchain: nightly-2019-12-02 use cortex_m_tm_alloc::allocator; use cortex_m_tm_executor::executor; #[allocator(lazy)] static mut A: Tlsf = { /* .. */ }; executor!(name = X, allocator = A);
TMアロケータ同様に、TM executorも「スレッドモード」のみでハンドラを取得できます。
get
コンストラクタはTM executorとTMアロケータを返します。
TM executorもCopy
トレイトを実装しますが、Send
やSync
は実装しません。
executorはタスクをspawn
するのに使います。
spawn
は、具体的なジェネレータを受け取り、Box
化し、内部キューに格納します。
spawn
自体は、ジェネレータ / タスクコードを実行しません!
タスクを実行するにはblock_on
APIを使います。
ふむ?とりあえずサンプルコードを見てみますか。
|> examples/tasks.rs
#[entry] fn main() -> ! { if let Some((x, _a)) = X::get() { x.spawn(move || { hprintln!(" A0").ok(); yield; hprintln!(" A1").ok(); // but of course you can `spawn` a task from a spawned task x.spawn(|| { hprintln!(" C0").ok(); yield; hprintln!(" C1").ok(); }); yield; hprintln!(" A2").ok(); // NOTE return value will be discarded 42 }); let ans = x.block_on(|| { hprintln!("B0").ok(); yield; hprintln!("B1").ok(); yield; hprintln!("B2").ok(); yield; hprintln!("B3").ok(); 42 }); hprintln!("the answer is {}", ans).ok(); } debug::exit(debug::EXIT_SUCCESS); loop {} }
上記コードを実行した結果は、次のようになります。
$ cargo run --example tasks --features="nightly" B0 A0 B1 A1 B2 C0 A2 B3 the answer is 42
もう一度サンプルコードに戻ってみると、下のジェネレータをexecutor X
でspawn
した時点では、まだ非同期コードは実行されません。
#[entry] fn main() -> ! { if let Some((x, _a)) = X::get() { x.spawn(move || { hprintln!(" A0").ok(); yield; hprintln!(" A1").ok(); // but of course you can `spawn` a task from a spawned task x.spawn(|| { hprintln!(" C0").ok(); yield; hprintln!(" C1").ok(); }); yield; hprintln!(" A2").ok(); // NOTE return value will be discarded 42 });
続く、block_on
でジェネレータが与えられると、実行を開始します。
B0
出力後、yield
すると、先ほどspawn
したタスクに制御が移り、A0
が出力されます。
let ans = x.block_on(|| { hprintln!("B0").ok(); yield; hprintln!("B1").ok(); yield; hprintln!("B2").ok(); yield; hprintln!("B3").ok(); 42 }); hprintln!("the answer is {}", ans).ok(); }
A1
ではさらにタスクをspawn
していますが、その時点では実行されておらず、B2
出力後のyield
で制御が移ってきます。
C0
後にyield
すると、A2
を出力するコードに制御が移っていますね。
block_on
で実行したジェネレータからは、戻り値 (42
) を受け取っています。
でもC1
が実行されていませんね?
そう、block_on
はspawnしたタスク全てが完了になることを保証しません。
単に、引数で渡されたジェネレータが完了になるまで、実行を進めるだけです。
block_on
をネストするとデッドロックする可能性があるため、TM executorではネストしたblock_on
呼び出しはパニックになるよう、実装されています。
#[r#async]
/ r#await!
block_on
をネストできないとすると、ジェネレータをどうやったら完了状態にできるのでしょうか?
r#await!
マクロを使います。ジェネレータを返す関数を簡単に書くために#[r#async]
アトリビュートもあります。
例として、割り込みハンドラから非同期にデータを受け取りたいとします。#[r#async]
を使って次のように書くことができます。
まずは、ジェネレータを簡単に書くためのアトリビュートです。
use core::ops::Generator; use heapless::{ spsc::Consumer, // consumer endpoint of a single-producer single-consumer queue ArrayLength, }; use gen_async_await::r#async; #[r#async] fn dequeue<T, N>(mut c: Consumer<'static, T, N>) -> (T, Consumer<'static, T, N>) where N: ArrayLength<T>, { loop { if let Some(x) = c.dequeue() { break (x, c); } yield } } // OR you could have written this; both are equivalent fn dequeue2<T, N>( mut c: Consumer<'static, T, N>, ) -> impl Generator<Yield = (), Return = (T, Consumer<'static, T, N>)> where N: ArrayLength<T>, { || loop { if let Some(x) = c.dequeue() { break (x, c); } yield } }
dequeue
はジェネレータを返す関数で、ジェネレータでは割り込みハンドラのProducer
からデータが送られてくるとSome
になるから、そこで値を返して、yield
しています。
んー?ジェネレータが複雑な型になったりすると有り難いのかな…?
アプリケーションは次のように書けます。
#[entry] fn main() -> ! { static mut Q: Queue<i32, consts::U4> = Queue(i::Queue::new()); let (p, mut c) = Q.split(); // send the producer to an interrupt handler send(p); if let Some((x, _a)) = X::get() { // task that asynchronously processes items produced by // the interrupt handler x.spawn(move || loop { let ret = r#await!(dequeue(c)); // <- ★ let item = ret.0; c = ret.1; // do stuff with `item` }); x.block_on(|| { // .. do something else .. }); } debug::exit(debug::EXIT_SUCCESS); loop {} }
うーん、難しい。dequeue(c)
はジェネレータを返しているので、x.spawn
の引数もジェネレータになると。
std_async::sync::Mutex
?
std_async
では、2つのタスク間でメモリを共有したい場合、Mutex
かRwLock
を使います。
task::spawn
APIの引数はSend
トレイトを実装したジェネレータです。
std_async
のジェネレータはマルチスレッドで動作し、並行実行される可能性があるからです。
use async_std::{sync::Mutex, task}; fn main() { let shared: &'static Mutex<u128> = Box::leak(Box::new(Mutex::new(0u128))); task::spawn(async move { let x = shared.lock().await; println!("{}", x); }); task::block_on(async move { *shared.lock().await += 1; }); }
TM executorは必ず同じコンテキストで動作し、タスクは1つずつ順番に実行されます。
そのためspawn
の引数はSend
を実装する必要がありません。
そのため、タスク間でデータを共有する際、Mutex
の代わりに、単にRefCell
かCell
を使うことができます。
あ、そうですね。
use core::cell::RefCell; #[entry] fn main() -> ! { static mut SHARED: RefCell<u64> = RefCell::new(0); if let Some((x, _a)) = X::get() { let shared: &'static _ = SHARED; x.spawn(move || loop { hprintln!("{}", shared.borrow()).ok(); yield; }); x.block_on(move || { *shared.borrow_mut() += 1; yield; *shared.borrow_mut() += 1; yield; }); } debug::exit(debug::EXIT_SUCCESS); loop {} }
うん、これはよくわかる。
実装を覗いてみよう
さて、ここまでがREADME
です (長い…) 。
r#await!
/ #[r#async]
r#await!
マクロは、わりと読めます。
|> gen-async-await/src/lib.rs
/// `e.await` -> `r#await(e)` // expansion is equivalent to the desugaring of `($g).await` -- see // rust-lang/rust/src/librustc/hir/lowering/expr.rs (Rust 1.39) // XXX Does `$g` need to satisfy the `: Unpin` bound? -- I think not because `$g` is drived to // completion so any self-referential borrow will be over by the time this macro returns control // back to the caller. This is unlike `futures::select!` which partially polls its input futures. // Those input futures may be moved around and then passed to a different `select!` call; the move // can invalidate self-referential borrows so the input future must satisfy `Unpin` #[macro_export] macro_rules! r#await { ($g:expr) => { match $g { mut pinned => { use core::ops::Generator; loop { match unsafe { core::pin::Pin::new_unchecked(&mut pinned).resume() } { core::ops::GeneratorState::Yielded(()) => {} core::ops::GeneratorState::Complete(x) => break x, } yield () } } } }; }
ジェネレータのステートマシンに沿った動作をしているようです。
#[r#async]
アトリビュートですが、修行をサボっていたせいで未だに手続きマクロがいまいち読めません!
が、関数の戻り値型をimpl Generator
型にし、関数内の処理をジェネレータにしているっぽいです。
/// `async fn foo() { .. }` -> `#[r#async] fn foo() { .. }` // NOTE the built-in `async fn` desugars to a generator and wraps it in a newtype that makes it // `!Unpin`; this is required because `async fn`s allow self-referential borrows (i.e. `let x = ..; // let y = &x; f().await; use(y)`). AFAICT, self referential borrows are not possible in generators // (as of 1.39) so I think we don't need the newtype #[proc_macro_attribute] pub fn r#async(args: TokenStream, item: TokenStream) -> TokenStream { if !args.is_empty() { return parse::Error::new(Span::call_site(), "`#[async]` attribute takes no arguments") .to_compile_error() .into(); } // snip let block = &item.block; quote!( #(#attrs)* #vis fn #ident #generics ( #inputs ) -> impl core::ops::Generator<Yield = (), Return = #output> #(+ #lts)* #where_clause { move || #block } ) .into()
collections
初期化時に任意のアロケータが指定できるコレクションです。
例えばVec
なら次のような感じです。
|> collections/src/vec.rs
pub struct Vec<T, A> where A: Alloc, { allocator: A, cap: usize, len: usize, ptr: Unique<T>, } impl<A, T> Vec<T, A> where A: Alloc, { // `new`で`Alloc`トレイトを実装するAを引数として渡す pub fn new(allocator: A) -> Self { let cap = if mem::size_of::<T>() == 0 { usize::max_value() } else { 0 }; Self { allocator, cap, len: 0, ptr: Unique::empty(), } } // snip
cortex-m-tm-alloc
Cortex-M
のスレッドモードでのみ使用できる「スレッドモードアロケータ」です。
get()
を呼んだ際、スレッドモードならアロケータインスタンスを、そうでなければNone
が得られるのでした。
Cortex-Mを知っていれば、実装は素直です。SCBのICSRの値を読み込んで、スレッドモードならSome
を、そうでなければNone
を返しています。
|> cortex-m-tm-alloc/src/lib.rs
pub unsafe fn get() -> Option<Self> { if cfg!(not(cortex_m)) { return None; } const SCB_ICSR: *const u32 = 0xE000_ED04 as *const u32; if SCB_ICSR.read_volatile() as u8 == 0 { // Thread mode (i.e. not within an interrupt or exception handler) Some(Private { _not_send_or_sync: PhantomData, }) } else { None } }
allocator
アトリビュートの実装は歯が立たなかったです。出直します。
cortex-m-tm-executor
Executor
の実装を見てみます。
アロケータとタスク配列、実行中かどうかを示すフラグをフィールドに持ちます。
|> cortex-m-tm-executor
pub struct Executor<A> where A: Alloc + Copy, { allocator: A, /// Spawned tasks tasks: UnsafeCell<Vec<Pin<Task<A>>, A>>, running: Cell<bool>, }
まず、spawn
です。
タスクをヒープ領域に作って、タスク配列に追加するだけです。
なので、spawn()
を呼んだだけではタスクが実行されなかったわけですね。
impl<A> Executor<A> where A: Alloc + Copy, { // snip pub fn spawn<T>(&self, g: impl Generator<Yield = (), Return = T> + 'static) { // this alternative to `GenDrop` produces larger heap allocations // let g = || drop(r#await!(g)); let task: Task<A> = Box::new(GenDrop { g }, self.allocator); unsafe { (*self.tasks.get()).push(task.into()); } } }
一方、block_on
では、ジェネレータが完了状態になるまで、タスク配列内のタスクを単純に順番に実行していることがわかります。
pub fn block_on<T>(&self, g: impl Generator<Yield = (), Return = T>) -> T { self.running.set(true); pin_mut!(g); loop { // move forward the main task `g` if let GeneratorState::Complete(x) = g.as_mut().resume() { self.running.set(false); break x; } let n = unsafe { (*self.tasks.get()).len() }; for i in (0..n).rev() { let s = { let task: TaskMut = unsafe { (*self.tasks.get()).get_unchecked_mut(i).as_mut() }; task.resume() }; if let GeneratorState::Complete(()) = s { // task completed -- release memory let task = unsafe { (*self.tasks.get()).swap_remove(i) }; drop(task); } } } }
ジェネレータの実装について少し。
結果を破棄するGenDrop
ジェネレータが実装されています。
resume
するとGeneratorState
が返ります。
GeneratorState::Complete
になると、値をdrop()
していることがわかります。
impl<G> Generator for GenDrop<G> where G: Generator<Yield = ()>, { type Yield = (); type Return = (); fn resume(self: Pin<&mut Self>) -> GeneratorState<(), ()> { match G::resume(self.g()) { GeneratorState::Yielded(()) => GeneratorState::Yielded(()), GeneratorState::Complete(x) => { drop(x); GeneratorState::Complete(()) } } } }
終わりに
完全に理解できなかったァ…。 とは言え、かなり理解は深まりました。
ただ、アプリケーションはともかく、実行エンジン作るのは、かなり大変そうですね…。
技術書典7でRustが関連する本/サークル一覧メモ
はじめに
随時、更新します。抜けや、間違いがあればご連絡下さい。
既刊ですが、私も「組込み/ベアメタルRustクックブック」を頒布しますので、よろしくお願いします(宣伝)。
新刊
い35C: esproject(エスプロジェクト)
く54D: OtakuAssembly
RustでOSやコンテナをネタにした内容が含まれるそうです。
け04D: ヤバイテックトーキョー
Writing a (micro)kernel in Rust in 12 days - 2.5th day - by nullpo-head 「Rustでマイクロカーネル書くやつのつづきやります」
こ31D: 井山梃子歴史館
Rustを用いてゲームエンジンを作ります.近年話題のEntity Component Systemがどのように実装されるのかを見ていき,またRustでどのようにグラフィックを扱えばよいのかを解説します.
こ32D: Team Jackalope
し41D: ふがふが(フガフガ)
- M5Stackではじめる組み込みRust
- 【委託】SePIA timers本
Rustの低レイヤ部分に興味がある人
せ34D: 肉と鍋(ニクトナベ)
- Rust on bare-metal Raspberry Pi 2nd
既刊
VxWorksの脆弱性「URGENT11」のテクニカルホワイトペーパーを読む①
はじめに
VxWorksは組込み機器では非常に有名なRTOSで、20億以上のデバイスに搭載されています。 今回の脆弱性URGENT11で影響を受けるデバイスは2億個以上であると報告されています。
脆弱性を報告したARMISが公開している上のページでは、医療機器やルータで任意コードを実行するエクスプロイトデモ動画が掲載されています。
URGENT11では11個の脆弱性が報告されています。Wind Riverセキュリティアドバイザリによると、その中の3つは、CVEのレーティングが9.8と非常に脅威度が高いものとなっています。
本記事では、ARMISが公開している脆弱性のテクニカルホワイトペーパーを読み、自分なりにまとめてみます。
今回は、レーティングが9.8のもののうち、リモート (LAN内) から任意コード実行が可能なCVE-2019-12256
のStack overflow in the parsing of IPv4 packets’ IP optionsに関する部分を読んでみます。
ホワイトペーパーに掲載されているコードスニペットでは一部解析できない部分があり、コードスニペットの解析では、一部推測が混じっています。
最終的には、「C言語で安全なコード書くの大変!」というお話になります。 ZenやRustのような配列 (またはスライス) が配列要素数情報を持っている言語であれば、(少なくとも)DoSまでは脅威度が下げられたように見えます。
URGETN11のホワイトペーパーではスタックオーバーフローと記載されていますが、スタックバッファオーバーフローなのではないかと疑っています。記事内では元のホワイトペーパーのままスタックオーバーフローとしています。
URGETN11
過去に発見されたTCP/IPスタックの脆弱性と同様のものが、ソースコードがクローズドなRTOSにもないかどうか、を研究しています。 研究にあたっては、ダウンロード可能なファームウェア (デバッグ情報付きのELF) を逆コンパイルしている、とあります。
概要
複数のSRR (Source Record Route) オプションを含む不正なIPパケットを送信すると、攻撃者が意図的にスタックオーバーフローを起こすことができ、任意コードが実行できます。これは適切な長さチェックを行わずに、SRRオプションをスタック上に確保したバッファにコピーすることに起因しています。
バックグラウンド情報
SRR (Source Record Route)
ソース・ルーティングとは、ネットワーク通信において、データの送信者が送信先のみでなく中継地点をも指定する経路制御方式のことである。
通ってきたルート情報をオプション内に記録していきます。オプションヘッダが3バイトあり、その後ろに、通ってきたルートのIPv4アドレスが記録されます。
記録できるルート情報は最大9件です。IPv4のオプションフィールドが (固定部分を除き) 最大で40バイトなので、オプションヘッダ (3バイト) + ルート情報 (4バイト×9 = 36バイト) まで、ということなのでしょう。
ICMPエラーパケット
IPパケットを処理している間にエラー状態になった場合 (不正なIPパケットを受け取った場合) 、ICMPエラーパケットを返信します。この場合、不正なIPパケットとは、送信先に到達できないパケットである、不正なオプションフィールドを含んでいる、などが挙げられます。
ICMPエラーパケットには、不正なIPパケットのコピーが含まれる場合が多いです。
はい、嫌な予感がしますね!
脆弱性
1つのIPパケットに複数のSRRオプションが含まれている状態は、エラーとして検出されます。しかし、脆弱性のあるVxWorksの不正IPパケット検出ロジックでは、そのエラーを検出する前に別のエラーを検出した場合、複数のSRRオプションが含まれていることを認識しないまま、不正IPパケットをコピーしてICMPエラーパケットを作成します。
ICMPエラーパケットとして確保されているオプションフィールドは、スタックに確保されている40バイトです。この領域に対して、最大で40バイトの大きさになるSRRオプションを複数回コピーしてしまい、スタックオーバーフローが発生します。
コード
ファームウェアのバイナリから逆コンパイルしたコードスニペットがホワイトペーパー内に掲載されています。
不正なIPパケットを受け取った場合、下のipnet_icmp4_send
関数からICMPエラーパケットを送信します。
int ipnet_icmp4_send(Ipnet_icmp_param *icmp_param, Ip_bool is_igmp) { Ipnet_icmp_param *icmp_param; Ipcom_pkt *failing_pkt; struct Ipnet_copyopts_param options_to_copy; // スタックに確保された40バイトの配列 struct Ipnet_ip4_sock_opts opts; // ... // 問題のオプションをコピーする関数を呼び出す ipnet_icmp4_copyopts(icmp_param, &options_to_copy, &opts, &ip4_info); // ... }
Ipnet_ip4_sock_opts
構造体の定義が掲載されていないので詳細は不明ですが、ホワイトペーパー内では、これは40バイトの配列である、と解説されています。
int ipnet_icmp4_copyopts(Ipnet_icmp_param *icmp_param, struct Ipnet_copyopts_param *copyopts_param, struct Ipnet_ip4_sock_opts *opts, void *ip4_info) { // ... while ( 1 ) { current_opt = ipnet_ip4_get_ip_opt_next( /* ... */ ); // ... if ( opt_type == 0x83 || opt_type == 0x89 ) { // IPオプションがSRR (LSRRもしくはSSRR) srr_ptr_offset = 39; srr_opt = (srr_opt_t *)&opts->opts[opts->len]; // ポインタオフセットは最大で39バイト目までしか指さないようにしているが、 // このオフセットが現在のオプション内で有効であるかどうか、は検証されていない if ( (int)current_opt[2] <= 39 ) srr_ptr_offset = current_opt[2]; offset_to_current_route_entry = srr_ptr_offset - 5; // ... // オプション内に記録されているIPアドレスを逆順に1つずつコピーする。 while ( offset_to_current_route_entry > 0 ) { memcpy((char *)srr_opt + srr_opt->length, current_route_entry, 4); current_route_entry -= 4; offset_to_current_route_entry -= 4; srr_opt->length += 4; } // 最新 (自身) のルート情報を追加する memcpy((char *)srr_opt + srr_opt->length, &icmp_param->to, 4); srr_opt->length += 4; total_opts_len = opts->len + srr_opt->length; } } } ... }
SRRオプションは次のデータ構造になっており、length
はオプションのバイト数で、pointer
はroute data
のオフセットです (オプション開始位置から数えるので、4
から始まります) 。pointer
(コード中のsrr_ptr_offset
) はlength
以下でなければならないはずですが、上記コードではそれがチェックされていません。
Loose Source and Record Route +--------+--------+--------+---------//--------+ | 0x83| length | pointer| route data | +--------+--------+--------+---------//--------+
脆弱性をつくには、次のようなIPオプションフィールドを送信します。
type | length | pointer | type | length | pointer |
---|---|---|---|---|---|
0x83 | 3 | 0x27 (39) | 0x83 | 3 | 0x27 (39) |
これでルート情報が欠落しているLSRRオプションが2連続で続くことになります。実際にはルート情報はありませんが、pointer
は0x27 (39)
を指しているので、9番目のルート情報が格納されている位置から逆順に、ルート情報をコピーしようとします。
このとき、コピー先はmemcpy((char *)srr_opt + srr_opt->length, current_route_entry, 4);
から、srr_opt
です。srr_opt
は、40バイトの配列であるopts
のどこかを指すポインタです。opts->len
を更新するコードが掲載されていないのですが、一番外側のwhileループが回るごとにオプションの長さ分加算されていると推測できます。
srr_opt = (srr_opt_t *)&opts->opts[opts->len];
上述の脆弱性をつくオプションフィールドをwhile文で1つずつ処理し、2つ目のオプションを処理する際には、opts->len
が3
か40
になっていると考えられます (オプションフィールドのlengthで更新していれば3
、コピーしたオプションフィールドの長さで更新していれば40
) 。そこを起点に、40バイトしか確保していない領域に、さらに最大で40バイトのコピーが発生します。
ここでコピーされる内容は、オプションフィールドに続く任意のデータです。
ということで、上位に戻って、ipnet_icmp4_send
のopts
を突き抜けてデータを書き込まれ、スタック上のリターンアドレスが、書き込まれた任意コードの先頭アドレスに書き変われば攻撃成功、なはずです。
int ipnet_icmp4_send(Ipnet_icmp_param *icmp_param, Ip_bool is_igmp) { Ipnet_icmp_param *icmp_param; Ipcom_pkt *failing_pkt; struct Ipnet_copyopts_param options_to_copy; // スタックに確保された40バイトの配列 struct Ipnet_ip4_sock_opts opts;
新しいプログラミング言語なら?
一番の直接的な問題点は、pointer
オフセットが有効なlength
の範囲内であるかどうか検証していないこと、でしょう。ここはロジックレベルの実装ミスであり、プログラミング言語レベルでは防ぎようがありません。
int ipnet_icmp4_copyopts( /* ... */ ) { // ... srr_ptr_offset = 39; srr_opt = (srr_opt_t *)&opts->opts[opts->len]; // ポインタオフセットは最大で39バイト目までしか指さないようにしているが、 // このオフセットが現在のオプション内であるかどうか、は検証されていない if ( (int)current_opt[2] <= 39 ) srr_ptr_offset = current_opt[2];
最終防衛線は、opts
が確保しているメモリの範囲外アクセスを検出することです。
// ... srr_opt = (srr_opt_t *)&opts->opts[opts->len]; // ... memcpy((char *)srr_opt + srr_opt->length, current_route_entry, 4); // ...
ZenやRustのような言語であれば、opts->opts
やsrr_opt
は配列要素数を型の一部として持つ配列 (へのポインタ) 、もしくは、スライスになります。このような言語であればopts->opts[opts->len]
で範囲外アクセスを行った場合や、スライスのコピー操作により範囲外アクセスを防ぐことが可能です。
どちらの言語でも
unsafe
なC言語のmemcpyと同等のコピー関数がありますが、そのような関数を使うと、もちろんC言語と同じ結果になります。
次のコードは、かなり問題を単純化した例をZenで書いたものです。
const std = @import("std"); fn copy_opts(opts: *[]u8) void { const length: usize = 38; // `opts`の38番目から42番目の要素を指すスライスを取得して、書き換えようとする var entry = opts.*[length..length + 4]; std.mem.copy(u8, entry, [_]u8{ 1, 2, 3, 4 }); } pub fn main() void { var opts: [40]u8 = [_]u8{0} ** 40; copy_opts(&opts[0..]); }
このコードは、パニックで停止します。copy_opts
関数の引数opts
が40個の要素しか持たないことがわかっているため、それを超える範囲のスライスを作ろうとすると、実行時にパニックが発生します。
index out of bounds main.zen:5:23: 0x2250b6 in copy_opts (main) var entry = opts.*[length..length + 4]; ^
お次はRustで書いたものです。
fn copy_opts(opts: &mut [u8]) { let length: usize = 38; // `opts`の38番目から42番目の要素を指すスライスを取得して、書き換えようとする let entry = &mut opts[length..length+4]; entry.copy_from_slice(&[1, 2, 3, 4]); } fn main() { let mut opts: [u8; 40] = [0; 40]; copy_opts(&mut opts); }
このコードも、パニックで停止します。
thread 'main' panicked at 'index 42 out of range for slice of length 40', src/libcore/slice/mod.rs:2555:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
パニック発生時にパニックを捕捉してハンドリングすることも可能です。
マルウェアを送り込まれた上で動作し続けることに比べると、システムが定義されたパニック状態に陥る方が、かなり被害が軽減できるでしょう。 このような最終防衛線がプログラミング言語レベルで用意されていることが、いかに大切かわかりますね!
技書博出展レポート〜初めての同人誌執筆で組込みRustの本を頒布しました〜
はじめに
記憶の新しいうちに、経緯などをまとめておきます。 自分用メモの側面が強いですが、今後初めて同人誌を頒布される方の参考になると嬉しいです。
BOOTHで物理本と電子版を販売しているので、もし良ければお買い求め下さい。 (物理本は倉庫への搬送作業は完了しており、入荷待ちです)
経緯
4月下旬、@hidemi_ishihara さんから出展のお誘いがありました。
組込みRustで書くことに決めて、今まで翻訳した組込みRustのドキュメントや、自分でこれまでやってきたことをクックブックとしてまとめることにしました。 クックブックにしたのは理由があり、急に執筆ができなくなっても読める内容になっている形式で執筆を進めたかったからです。 第二子の出産予定日が6月初旬だったため、この判断は良かったと思います。
性格的にギリギリにやるのは性分ではないこともあり、手持ちで書籍化できそうなものが、組込みRustかZephyrしかなかった、というのもありますが。 (Zephyrはそこまで思い入れないですからねぇ…)
執筆の進行
gitの履歴を見てみると、4/29にレポジトリを作っていました。 2週間後の5月中盤には、頒布した内容の半分は書けています。 この頃は、平日は1時間前後、休日は2〜3時間執筆していました。
ここまでは順調でしたが、まさかの第二子が3週間早く産まれてきてしまう、というハプニングに見舞われます! ガクッと執筆ペースが落ちて、残り半分の内容を埋めるのに6週間ほどかかっています。 この頃は、平日30分時間が取れれば良い方で、休日も2時間執筆できれば万々歳でした…。
7月に入り、組版作業に入ることを決めました。 この時点で、まだ書きたかった内容を全て切り捨てました。
初めてなので、どのくらい製本すれば良いかわかりませんでした。 とりあえずtwitterランドの住人に聞いたろ、ということで、聞いてみると、100イイねついたので、100冊刷ることにしました。
組込み / ベアメタルRustクックブック
— 錆ありはぐれベアメタル (@LDScell) June 29, 2019
目次はこんな感じで80ページくらい。
わからんけど、紙媒体1000円くらい?
初同人誌なのでクオリティはお察し。
7/27(土)技術書博覧会で販売予定。
買ってあげても良いよ、という方はイイね下さい。
印刷部数の参考にします。 pic.twitter.com/5U3NEQhMD3
製本をどこに頼もうか調べていたところ、@hidemi_ishihara さんから、いつもポプルスさんで印刷している、という情報を得ました。 調べる時間がもったいないので、「じゃあ、そこで!」ということにして、早速アカウント作って見積もりと製本予約をしました。
ということで、組版作業を都合2週間ほどやっていました。
こんなに時間がかかったのは少し理由があって、mdbook
というRust製のドキュメントビルダーで原稿を執筆していました。
mdbook
には未完成品ですがEPUB形式で出力する機能があります。
mdbook
から出力されたEPUB形式の原稿を、calibreという電子書籍エディタで編集していました。
慣れないCSSをいじったり、なぜかPDF出力するときにコードブロックの強調が消えてしまうバグと不毛な争いをしていました。 ということで、2週間前に組版を始めたのも、良い判断でした。
表紙作成もこの辺りの期間にやりました。 割と面倒で、時間かかりました。
7/12には入稿を済ませて、一段落つきました。
当日まで
不安しかねぇ!
というのも、初めての同人誌執筆(厳密にはD論という名の同人誌製本していますが)で会場直接搬入なので、実物が読めたものになっているかどうか、わかりません!
当初より今回の本は、組込みRustの知名度向上のため、価格を安くしてばら撒く作戦でした。
さらにmdbook
は静的なページを作るツールなので、HTML版も合わせて配れば、安いし誰も怒らないでしょ!みたいな開き直りをすることで、心の安寧を保ちました。
その裏で、名刺を自炊したり、ダウンロードカード作ったりしていました。
後、twitterやブログで必死の宣伝活動していました。 どこかの記事で、技書博は集客1000人を目指している、と聞いて、来場者の10%もこんなニッチな本を買うわけがない!という焦りがありましたね。
値段は500円にしたので、とりあえず500円玉を30枚、お釣りとして用意しました。
当日
とにかく本のできを見て、一安心しました。 これなら500円でも怒られはしないでしょ!というクオリティになっていました。 少し上下左右の余白取りすぎた気がしますが、文字が詰まっている圧迫感も感じないので、悪くない気がします。 読者の皆様からの感想をお待ちしております。
@hidemi_ishihara さんに導かれるまま、見本誌にカバーかけたり、スペースの準備をしました。 カッターナイフも何度かお借りしました。カッターナイフ、意外と要るで?
午前中は、全然売れなかったです! 11時〜12時の間の売上は3冊でした。 内心すごい焦燥感に駆られていました。
イベント自体は、ゆっくりスペースを回れて、著者とお話しする余裕が十分にあるので、良い感じだなぁ、と思いました。 物が売れない焦燥感を除けば!
13時以降、徐々に売上が伸びていき、14時〜15時くらいの間に20冊近く販売できました。 最終的には、53冊頒布しました。
一般入場者が640名ほどとのことなので、このニッチなジャンルで53冊はだいぶ頑張った方ではないでしょうか笑 ゆっくり見れたり、話した結果ご購入下さった方もいらっしゃったので、購入する側としても満足度が高かったのかもしれません。
中には、「Rustはやったことないのだけど、気になるし、安いから買います(意訳)」という方や「Rust勉強してからまた来ます」、と言って下さった方も複数名いらっしゃったので、狙いは良かったと思います。
頒布時間終了後、40冊はBOOTHさんに入庫することにしました。 スーツケース持ってきておけば、持って帰って技術書典7で頒布できたなぁ、と思いましたが、後の祭りですね。 荷物軽くするために、リュック1つで行ったのが間違いでした…。
技術書典7では、また50冊くらい刷ることにします。
お金の話
100冊製本して、約37,500円でした。 ばらまきたいので、1冊500円で頒布することにしました。
現状、BOOTHでの売上も含めて、なんとか損益分岐点に到達しました! お買上げ、ありがとうございます! Boostまでして下さる方もいらっしゃって、非常にありがたいことです。
今回は、赤字にさえならなければ勝ちなので、満足です!
今後について
HTML版は、適宜更新していこうと考えています。 時間不足でバッサリ切ってしまった部分が残っているので、そちらの加筆も行う予定です。 組版はぼちぼち手間がかかるので、PDF版および紙版の更新は余裕があれば、やります。
読者の皆様からフィードバックがあれば、加筆修正する大きなモチベーションになるため、フィードバックをお待ちしております。
7/27(土) 技書博で組込み/ベアメタルRustクックブックを販売します!
はじめに
宣伝です!
来週開催される技術書同人誌博覧会にて、組込み/ベアメタルRustクックブックを販売します。 A-9 AQUAXISさんのブースにご一緒させて頂きます。 ブース主の石原ひでみさんはFPGAの薄い本とMarkdown組版の本を、みつきんさんはNuttxの本を頒布されます。
販売情報
価格は、500円です。 PDF版とHTML版がデフォルトで含まれます。 当日の会場にてお買い上げいただくと、先着で100名様に、紙媒体をお渡しします。 1冊のご購入につき、紙媒体を1冊まで、お渡しします。
当日お越し頂けなかった方にも、後日何らかの形でPDF版とHTML版が入手できる手段を用意します。
目次は、次の通りで、表紙込で78ページです。
- はじめに
- 環境構築
- ベアメタルテクニック
- no_std
- panic
- print!マクロ
- リンカ
- アセンブリ
- メモリアロケータ
- entryポイント
- ツール
- Cargo
- コンパイラサポート
- rustc
- ライブラリ / フレームワーク
- heapless
- no_stdクレート
- svd2rust
- RTFM
- Tock
- testing
- FFI
- RustからCを呼ぶ
- CからRustを呼ぶ
- ケーススタディ Zephyr
- 組込みLinux
- ビルド/テスト
- Yocto
- あとがき
正直なところ、HTML版が一番見やすいので、HTML版に500円払う気持ちで購入して頂けると嬉しいです。
ところで
100イイねついたから、100部刷ったので、買って下さいね!(切実)
組込み / ベアメタルRustクックブック
— 錆ありはぐれベアメタル (@LDScell) June 29, 2019
目次はこんな感じで80ページくらい。
わからんけど、紙媒体1000円くらい?
初同人誌なのでクオリティはお察し。
7/27(土)技術書博覧会で販売予定。
買ってあげても良いよ、という方はイイね下さい。
印刷部数の参考にします。 pic.twitter.com/5U3NEQhMD3
LLVM IRファイルを読み込む
はじめに
ファイルに出力されたLLVM IRをC++で読み込んで遊んでみます。
環境
$ clang++ --version clang version 8.0.1-svn360950-1~exp1~20190517004233.70 (branches/release_80) Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
$ llvm-config --cxxflags --ldflags --system-libs --libs core -I/usr/lib/llvm-8/include -std=c++11 -fno-exceptions -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -L/usr/lib/llvm-8/lib -lLLVM-8
LLVM IRファイルの読み込み
アプリケーションの引数にLLVM IRファイルパスを指定する想定です。
#include <llvm/IR/LLVMContext.h> #include <llvm/IR/Module.h> #include <llvm/IRReader/IRReader.h> #include <llvm/Support/SourceMgr.h> int main(int argc, char** argv) { if (argc < 2) { llvm::errs() << "Expected an argument - IR file name\n"; exit(1); } llvm::LLVMContext Context; llvm::SMDiagnostic Err; auto module = llvm::parseIRFile(argv[1], Err, Context); if (!module) { Err.print(argv[0], llvm::errs()); return 1; } return 0; }
llvm::parseIRFile
に、ファイルパスを与えるだけで、llvm::Module
(のunique_ptr) を得ることができます。
下のコマンドでLLVM IRファイルが読み込めます。
clang++ main.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o ir_reader ./ir_reader main.ll
少し内部を覗く
main.ll
にはmain
関数があり、いくつか命令があります。
そのオペコードを表示してみます。
次のようなコードで実現できました。
auto func = module->getFunction("main"); std::cout << "function name: " << func->getName().data() << std::endl; for (auto &bb: *func) { auto i = 0; for (auto &instr: bb) { std::cout << "%" << i << ": " << instr.getOpcodeName() << std::endl; i++; } }
$ ./ir_reader main.ll function name: main %0: alloca %1: bitcast %2: call %3: getelementptr %4: load %5: call %6: ret
お手軽ですね!
Rust × 組込みで前代未聞のInterfaceオフ会レポート
はじめに
昨日、2019年6月17日、巣鴨のCQ出版社様セミナールームにおいて記念すべき組込みRustのオフ会が開催されました! 今回のオフ会は、雑誌掲載前にオフ会を開催する、という前代未聞のオフ会、とのことでした。
非常に盛り上がったオフ会になったので、そのレポートです。
プレゼン
1. 組込みRustのすゝめ
— 錆ありはぐれベアメタル (@LDScell) 2019年6月17日
2. PC上でRustドライバ開発
3. Rustで組込みOS自作
4. モダンマイコンOS Nuttxでlibstdを動かす#Ifcqmeetup
さらに、LTとして2つの発表がありました。
- M5StackでRust
- Rustファームウェア搭載の自作キーボード
組込みRustのすゝめ
まず、私から導入のお話をさせていただきました。
最初に会場内でアンケートを取ったところ、
- まだRustをやったことがない、が半分ほど
- 入門はした、はまさの0人
- 組込み以外でRustをやっている、が2割弱
- 組込みRustをやっている、が3割弱
といった感じでした。 知らない人か、プロしかいない!という両極端な結果が印象的でした。
導入と言いつつ、所有権システムの説明を少し入れており、ここの部分はRust知らない方々にどの程度理解して頂けたか、が気になっています。
Rustでチョット気軽にセンサドライバ開発
@ryochack さんからホストPC上で組込みセンサドライバ開発を行う発表です。
「いつまで僕らはC/C++を使い続けなければならないのか…」
全くその通りです!
発表内容は、下のデバイスを使って、PC上で直接Rustのセンサドライバを開発し、マイコンに持っていけるようにしよう!というものでした。
MPSSEのOSS実装、libmpsseからbindgenを使って、Rustバインディングを生成しています。 その時のラッパ作成の苦労話が、涙なしでは語れません。
お行儀の悪いCのコード!
— 錆ありはぐれベアメタル (@LDScell) 2019年6月17日
ちゃんとconstを付けないとbindingを作りにくので、Cのコードもちゃんと作ろう!#Ifcqmeetup
デモされていたもののレポジトリは、こちらです。
Rustで始める自作組込みOS
@garasubo さんから、組込みOSを自作したお話しです。
RustでOSを作る際の良かった話や、苦労話が生々しい発表でした! ライフタイムやテストフレームワークなど、良かった点もありますが、データ構造の実装が難しい、クレートが不足している、という課題もあります。
自作OSをRustで開発すべきか
— 錆ありはぐれベアメタル (@LDScell) 2019年6月17日
メリットは大きいが、発展途上なので、リスクも大きい。#Ifcqmeetup
自作OSのレポジトリは、こちらです。
Nuttxでlibstdを動かす
杉野さんから、POSIX likeな組込みOSであるNuttxでlibstdを動かした発表です。 今のところ、資料公開はされていません。
茨の道をひたすら突き進むような発表を前に、参加者一同、「これはツラすぎないか…」みたいな空気になっていましたが、ある程度動くようになっていて、圧巻の内容でした!
Rustの安全性や利便性を最大限享受するためにlibstdを使いたい、というのは組込みRustやっている人が共通で持っている望みだと思います。 リンカのバグを踏んだり、OS側にAPIが不足していたり、OS側とRust側とで構造体のメンバが違ったり、と数々の難関をくぐり抜け、動いた先は--。
M5 StackをRustで動かすまで
@ciniml さんから、ESP32上でRustを動かすようにするまでに試したことの発表です。
www.slideshare.net
Rust (というかLLVM) は公式にXtensaに対応していないため、LLVMのforkを使ってrustcのXtensa対応を行われていました。 ESP-IDF側のmalloc/freeをGlobalAllocでラップして、libcoreの機能を使えるようにしていました。
@ryochackさんと同様に、bindgenを使用されており、その苦労話もありました。
embedded_graphicsクレートを使って、画像を描画することまでできていて、次はWi-Fiを動かしたい、とのことです!
Rustで書いたファームウェアが乗った自作キーボードのデモ
@KOBA789 さんからキーボード自作のインターン向けに作成したRustファームウェア搭載の自作キーボードについてデモがありました。
【オフ会】
— コンピュータ技術実験雑誌「Interface」(毎月25日発売) (@If_CQ) June 17, 2019
デモ:Rustで自作キーボードのデモ
クックパッド開発者ブログってところで
クックMyパッド的に紹介されているらしい#ifcqmeetup pic.twitter.com/q0ulROP6iI
会社での無茶ぶり?から産まれたようです。 ステートマシン実装時、ステートが移るときにデータを引き継ぐところに苦労された、ということでした。
懇親会
組込みRustをやっている者同士のぶっちゃけトークが楽しかったです。
身代わりパターンめっちゃ使うよね〜、とかunsafe
使いたくないんだけど、やりたいことができなくてツラい、などなど。
組込みRustでまだまだ不足している部分や、課題もありますが、やらないといつまで経っても使えるようにならないから、やる!という認識が共有されたのも、個人的には非常に嬉しかったです。
まだRustを触っていない方々から、実際のところどーなの?的な質問にも正直な回答をさせていただきました。
告知
まさかの記事やってないのに
— コンピュータ実験雑誌の編集ラームー (@if_CQ_naka) 2019年6月17日
「ポインタが使えるモダンC++風言語Rust×組み込みの研究」
のオフ会やっちゃったので
逆順で特集執筆者会議やります
興味がある方はご連絡ください
あと新たな試みなので至らないところがあったら許してください#ifcqmeetup pic.twitter.com/WjUsqKWYoM