satumaimoの備忘録

個人的なメモ中心

【Interface 2023年7月号】Linux+VSCode+DockerによるTry Kernel開発環境の構築

はじめに

本記事では,「Interface 2023年7月号」にて掲載されているRTOS「Try Kernel」の,Linux+VSCodeを用いた開発環境構築手順を説明します.

実のところ,「Try Kernel」の開発環境はPicoprobeによるRaspberry Pi Picoのデバッグ環境であり,関連する情報は公式,非公式含めて十分にあるのですが,本記事は一つの事例として参考にしていただけると幸いです.

本記事の後半では,本記事が提供するリポジトリを使用した,Dockerを活用したVSCodeの機能であるDevcontainerによる,半自動の開発環境構築についても説明します.

なお,本記事は非公式であり,あくまでいち読者がTry Kernelの開発環境構築を行った事例であることをご了承ください.

前提条件

本記事で使用するハードウェア

※のあるものは秋月電子で購入可能

開発環境

ハードウェアの構築

はんだ付け

Raspberry Pi Picoにピンヘッダをはんだ付けします.

はんだ付けを行わずにブレッドボードでPicoを接続する事例もあったのですが,私の環境では接触不良に起因すると思われるエラー (後述) が発生したので,可能であればはんだ付けするのが良いと思います.

ちなみに,はんだ付けは個人的にこちらの動画が参考になりました.

配線

Try Kernel搭載用Picoにシールドを装着し,公式ドキュメントGetting startedのAppendix A: Using Picoprobe/Picoprobe Wiringに従って以下のように配線します.

(A: Picoprobe搭載Pico,B: デバッグ対象のPico)
Pico A GND -> Pico B GND
Pico A GP2 -> Pico B SWCLK
Pico A GP3 -> Pico B SWDIO
Pico A GP4/UART1 TX -> Pico B GP1/UART0 RX
Pico A GP5/UART1 RX -> Pico B GP0/UART0 TX

Raspberry Pi Picoの配線 (右がPicoprobe)

開発環境の構築 (ホストOS)

本節では,基本的に前述のGetting startedを踏襲しつつ,ホストOS上に直接環境構築する手順を説明します.

ビルド/デバッグ環境の構築

事前準備

以降の手順を進める前に,以下の操作を行います. (参考)

$ sudo apt update
$ sudo apt install binutils-multiarch gdb-multiarch
$ cd /usr/bin
$ ln -s /usr/bin/objdump objdump-multiarch
$ ln -s /usr/bin/nm nm-multiarch

OpenOCDのビルド/インストール

Getting startedのAppendix A: Using Picoprobeに従い,OpenOCDをビルドしてUbuntuにインストールします.

$ cd ~/
$ mkdir pico
$ cd ~/pico
$ sudo apt install automake autoconf build-essential texinfo libtool libftdi-dev libusb-1.0-0-dev pkg-config
$ git clone https://github.com/raspberrypi/openocd.git --branch rp2040 --depth=1
$ cd openocd
$ ./bootstrap
$ ./configure
$ make -j4
$ sudo make install

SDKのダウンロード

PicotoolとPicoprobeをビルドするために,Getting startedのChapter 2. The SDKに基づいてSDKをダウンロードします.

$ cd ~/pico
$ git clone https://github.com/raspberrypi/pico-sdk.git --branch master
$ cd pico-sdk
$ git submodule update --init
$ cd ..
$ git clone https://github.com/raspberrypi/pico-examples.git --branch master

Toolchainのインストール

Raspberry Pi Pico (Arm) へのクロスコンパイルを行うための,ツール群をインストールします.

$ sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential

Picotoolのビルド

後述のPicoprobeをRaspberry Pi Picoへインストールするために,Getting startedのAppendix B: Using Picotoolに基づいてPicotoolをビルドします.

$ cd ~/pico
$ git clone https://github.com/raspberrypi/picotool.git --branch master
$ cd picotool
$ mkdir build
$ cd build
$ export PICO_SDK_PATH=~/pico/pico-sdk
$ cmake ../
$ make

Picoprobeのビルド/インストール

Getting startedのAppendix A: Using Picoprobeに基づき,Picoprobeをビルドします.

$ cd ~/pico
$ git clone https://github.com/raspberrypi/picoprobe.git
$ cd picoprobe
$ git submodule update --init
$ mkdir build
$ cd build
$ export PICO_SDK_PATH=../../pico-sdk
$ cmake ..
$ make -j4

ビルドして得られたpicoprobe.uf2を,以下の手順でRaspberry Pi Picoへロードします.

  1. Raspberry Pi PicoをPCとBOOTSEL modeで接続する.
    • Raspberry Pi PicoのBOOTSELボタンを押しながらPCとUSBで接続すると,BOOTSEL modeで接続できます.
  2. 以下のコマンドを実行する.
$ cd ~/pico/picotool/build/
$ sudo picotool load ~/pico/picoprobe/build/picoprobe.uf2
$ sudo picotool reboot

バイスファイルの権限変更

一般ユーザがVSCodeデバッグを行えるよう,以下のようにデバイスファイルの権限を変更します.

sudo chmod -R 777 /dev/bus/usb/

VSCodeの設定/動作確認

Getting startedのChapter 7. Using Visual Studio Codeを参考に,VSCodeの設定を行います.

なお,本記事ではVSCodeが既にUbuntuへインストールされている前提で説明します.

拡張機能のインストール

以下の拡張機能をインストールします.

動作確認用Try Kernelのclone

開発環境の動作確認用として,既製のTry Kernelをcloneしておきます.

cd (適当なディレクトリ)
git clone -b build_cmake https://github.com/ytoyoyama/trykernel.git

デバッグ用の設定

VSCodeでのデバッグに必要となるsettings.jsonとlaunch.jsonを,pico-examplesから流用します.

cd ./trykernel
cp ~/pico/pico-examples/.vscode/* ./.vscode/

また,settings.jsonおよびlaunch.jsonにそれぞれ以下の設定を追記します.

    "cmake.environment": {
        "PICO_SDK_PATH": "~/pico/pico-sdk/"
    }
    "openOCDLaunchCommands": [
        "adapter speed 5000"
    ]

動作確認

周辺機器の取り付け

誌面を参考に,以下のように周辺機器を取り付けます.

Try Kernel実行用Picoへの周辺機器取り付け

なお,USBによる給電がPicoprobe用のPicoでのみ行われている場合,Try Kernel実行用Picoに接続されている周辺機器に給電がされないようです.

そのため,Picoprobe用,Try Kernel実行用両方のPicoでUSB給電を行ってください.

デバッグ/実行

VSCodeディレクトtrykernelを開き,左のバーから「実行とデバッグ」タブを選択します.

この際,kitの選択が求められた場合はarm-none-eabiを指定してください.

その後,左上の「デバッグの開始ボタン (緑色のアイコン) 」をクリックし,main関数でbreakされればデバッグに成功です.

デバッグを続行した結果,液晶モジュールに文字列「Try Kernel」が出力されれば,周辺機器も正常に動作しています. (ジェスチャーモジュールに手を振りかざすと,液晶モジュールの出力が変化するのを確認できます.)

なお,OpenOCDからunable to find a matching CMSIS-DAP deviceというエラーが出力された場合,原因として以下が考えられます.

配線の接触不良について,私の場合は,Picoにはんだ無しでピンヘッダを差してた状態から,はんだ付けを行うことによって解決しました.

アクセス権限不足が原因である場合は,デバイスファイルの権限変更 (前述) を行ってください.

Devcontainerを用いた環境構築の半自動化

本節ではDevcontainerを用いて,環境構築をコンテナ上に,かつ半自動的に行う手順を説明します.

環境構築には本記事が提供するリポジトリを使用します.

ハードウェア周りは本記事の前半を参照してください.

リポジトリのclone

git clone https://github.com/satumaimo10/trykernel_devenv.git

Devcontainerの起動

cloneしたリポジトリVSCodeで開き,右下に現れるポップアップの「コンテナーで再度開く」をクリック,もしくはコマンドパレット (Ctrl-Shift-P) にDev Containers: Open Folder In Containerと入力してDevcontainerを起動します.

Picoprobeの書き込み

PicoをBOOTSEL modeでコンテナの動作するホストマシンに接続し,VSCode上のターミナルで/tools/load_picoprobe.shを実行します.

動作確認

ホストOSで環境構築した場合と同様に,既製のTry Kernelを使用して動作確認を行います.

ホストOSの場合と同様のハードウェア構成 (「周辺機器の取り付け」節を参照) にしたのち,以下を実行します.

$ cd /workspace/
$ git clone -b build_cmake https://github.com/ytoyoyama/trykernel.git
$ cp trykernel/* ./

その後,VSCodeのウィンドウを再読み込みし,kitにarm-none-eabiを指定すれば準備完了です.

ホストOSの場合と同様の手順で,デバッグ機能,およびTry Kernelの正常な動作を確認できます.

おわりに

本記事では,Linux+VSCode+Docker (Devcontainer) を使用したTry Kernelの開発環境構築手順について説明しました.

Try Kernel環境構築の一助になれば幸いです.

参考記事

【Python】os._exit()で終了するプログラムではファイルを書き出す時にflushする

はじめに

os._exit()を使用するPythonプログラムでは,ファイルへ書き込まれたデータがユーザランドのバッファからOSに移る前にプロセスが終了する場合があります.

この場合,プログラムで行ったはずのファイルへの書き込みは,実際のファイルシステムには反映されません.

このような問題は,flushを行えば解決できます.

例1:一般的なファイル書き込み

以下のプログラムを考えます.

import os

with open("hoge.txt", 'w') as f:
    f.write("hoge")
    os._exit(0)

(環境次第ですが)上記プログラムを実行しても,hoge.txtに文字列hogeは書き込まれません.

このとき,flush()を用いれば文字列hogeが書き込めるようになります.

import os

with open("hoge.txt", 'w') as f:
    f.write("hoge")
    f.flush()           # バッファ内のデータをOSに渡す
    os._exit(0)

Pythonclose()はflushを行う処理も含んでいるようなので,以下のコードでも文字列hogeが書き込まれます.

import os

with open("hoge.txt", 'w') as f:
    f.write("hoge")
os._exit(0)

例2:標準出力のリダイレクト

個人的な本題はこちらです.

以下のプログラムを考えます.

import os

print("hoge")

os._exit(0)

リダイレクトを指定せずにこのプログラムを実行した場合,文字列hogeはターミナルに問題なく出力されます.

しかし,以下のようにリダイレクト先にファイルを指定した場合,文字列hogeは出力されません.

$ python3 hoge.py | tee hoge.txt
(出力なし)

この場合は,sys.stdout.flush()を呼び出すとリダイレクト先に標準出力がflushされます.

import os
import sys

print("hoge")
sys.stdout.flush()

os._exit(0)

おわりに

「標準出力をリダイレクトすると出力されなくなる/出力が途中で終わる」という問題を,flushを関連付けて言及している記事が見つからなかったので,このような記事を書きました.

Pythonでやむを得ずos._exit()を呼び出す場合は,flushを意識してプログラムを書く必要がありそうです.

参考文献

雑多なオペレーティングシステムインターフェース — Python 3.11.3 ドキュメント

ファイルオブジェクトのcloseはflushも行う。確実にしたければfsync - 静かなる名辞

【C言語】静的ライブラリに未定義関数を含める

はじめに

ライブラリの中には,未定義の関数が存在する場合があります.

例えばNewlibでは,システムコールに相当する部分 (_sbrk()など) がライブラリ上で定義されていません.

開発者がこのようなライブラリを使用する場合,未定義の関数は開発者自身が実装しなければなりません.

初歩的な話題ですが,この辺りの話を整理するために,実際に未定義の関数を含んだライブラリを作ってみました.

ライブラリの実装

以下のようなライブラリを実装しました.

#include <stdio.h>
#include "hoge.h"
#include "_huga.h"

void printHoge()
{
  printHuga();
  puts("hoge");
}
void printHoge();
  • _huga.h
void printHuga();

上記のプログラムをコンパイルし,arコマンドで静的ライブラリにします.

$ gcc -c hoge.c
ar rcs libhoge.a hoge.o

以上でライブラリは完成です.

ライブラリの使用

作成したライブラリを実際に使ってみます.

まずは,未定義関数を定義せずにライブラリの使用を試みます.

  • main.c
#include <stdio.h>
#include "hoge.h"

int main(void)
{
  printHoge();
  return 0;
}

このコードをコンパイルしてみます.

$ gcc -I. -L. -lhoge -o main main.c
/usr/bin/ld: ./libhoge.a(hoge.o): in function `printHoge':
hoge.c:(.text+0x9): undefined reference to `printHuga'
collect2: error: ld returned 1 exit status

関数printHuga()が定義されていないため,リンク時にエラーとなりました.

次に,未定義関数を定義してライブラリの実行を試みます.

  • main.c (修正版)
#include <stdio.h>
#include "hoge.h"
#include "_huga.h"

int main(void)
{
  printHoge();
  return 0;
}

void printHuga(void)
{
  puts("huga");
}

このコードをコンパイルし,実行してみます.

$ gcc -I. -L. -lhoge -o main main.c
$ ./main
huga
hoge

正常に実行できているのが確認できました.

終わりに

(少なくとも静的ライブラリでは) ライブラリに未定義関数を含められることが,実装を通して確かめられました.

【個人的メモ】gcc -Dでインクルードファイルを定義する場合は"<"と">"をエスケープする

GCCの初歩的な話題ですが、2時間近くハマったのでメモします。

-Dオプションとは

-D name

Predefine name as a macro, with definition 1.

-D name=definition

The contents of definition are tokenized and processed as if they appeared during translation phase three in a #define directive. In particular, the definition is truncated by embedded newline characters.

(gcc(1) — Linux manual pageから引用)

-Dは、#defineのようにマクロを定義できるオプションです。

-Dを用いたインクルードファイルの定義

あるソフトウェアに、以下のようなコードが含まれていました。

hoge.c

#include <stdio.h>
#include INCLUDE_FILE

int main(void)
{
  printf("%d\n", INCLUDED_CONSTANT);
  return 0;
}

hoge.h

#define INCLUDED_CONSTANT 10

ファイル構成は以下の通りとします。

Makefile
hoge.c
Inc/
└ hoge.h

このソフトウェアのMakefileには、以下のようなコマンドでhoge.cをコンパイルするよう記述されていました。

gcc -I./Inc -DINCLUDE_FILE=<hoge.h> hoge.c

しかし、make実行時に以下のエラーが発生しました。

-bash: hoge.h: そのようなファイルやディレクトリはありません

さらに、「hoge.cの中身が空になる」という現象も起きました。

エラー文に"-bash"とあるにも関わらず私はインクルードパスの指定に問題があるか、あるいはGCCのバグだと当初は考えていました。

しかし、実際は"<"と">"がリダイレクトの記号として扱われていたのが原因でした。

従って、以下のように-Dの引数をシングルクォーテーションで囲めば正常にコンパイルできます。

gcc -I./Inc -D'INCLUDE_FILE=<hoge.h>' hoge.c

なお、先述のmanページにはこのような記述もありました。

If you wish to define a function-like macro on the command line, write its argument list with surrounding parentheses before the equals sign (if any). Parentheses are meaningful to most shells, so you should quote the option. With sh and csh, -D'name(args...)=definition' works.

最初からmanページをしっかり読んでおけば良かったですね…。

結論

シェルでコマンドを実行するとき、(リダイレクトする場合を除いて)"<"と">"は必ずエスケープする。

インラインアセンブラで.textセクション内にデータを挿入する

はじめに

書籍「実践バイナリ解析」第6章の終わりに、以下の練習問題がありました。

objdumpを混乱させるプログラムを記述してみよう。

という訳で、本記事ではobjdumpによる解析を妨げるHello Worldプログラムの作成を試みます。

方針

objdumpのような線形逆アセンブラは、解析対象のセクション内にデータが含まれていた場合、そのデータを誤って命令として認識してしまう可能性があります。

従って本記事では、.textセクションのmainシンボル内にデータを挿入してobjdumpによる解析を妨げられないかを試します。

実装

以下のプログラムを実装しました。

#include <stdio.h>

int main(void){
  char* a;
  __asm__("jmp HOGE \n\t"
          "HELLO: \n\t"
          ".string \"Hello World!\" \n\t"
          "HOGE: \n\t"
          "mov %0, OFFSET FLAT:HELLO \n\t" 
          : "=r" (a)
          );
  printf("%s\n", a);
  return 0;
}

インラインアセンブラを用いて.string \"Hello World!\"をコード内に挿入し、かつ挿入したデータを命令として実行しないようにjmp命令を使用しています。

なお、上記プログラムはintel構文を使ってインラインアセンブリを記述しているので、コンパイル時には-masm=intelオプションを追加する必要があります。

コンパイル&実行

コンパイル

$ gcc -masm=intel -o hello.elf hello.c

実行

$ ./hello.elf
Hello World!

objdumpによる逆アセンブル

objdumpの実行

$ objdump -d -M intel hello.elf

実行結果(抜粋)

0000000000400526 <main>:
  400526:       55                      push   rbp
  400527:       48 89 e5                mov    rbp,rsp
  40052a:       48 83 ec 10             sub    rsp,0x10
  40052e:       eb 0d                   jmp    40053d <HOGE>

0000000000400530 <HELLO>:
  400530:       48                      rex.W
  400531:       65 6c                   gs ins BYTE PTR es:[rdi],dx
  400533:       6c                      ins    BYTE PTR es:[rdi],dx
  400534:       6f                      outs   dx,DWORD PTR ds:[rsi]
  400535:       20 57 6f                and    BYTE PTR [rdi+0x6f],dl
  400538:       72 6c                   jb     4005a6 <__libc_csu_init+0x46>
  40053a:       64 21 00                and    DWORD PTR fs:[rax],eax

000000000040053d <HOGE>:
  40053d:       48 c7 c0 30 05 40 00    mov    rax,0x400530
  400544:       48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  400548:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  40054c:       48 89 c7                mov    rdi,rax
  40054f:       e8 ac fe ff ff          call   400400 <puts@plt>
  400554:       b8 00 00 00 00          mov    eax,0x0
  400559:       c9                      leave  
  40055a:       c3                      ret    
  40055b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

HELLOシンボル内に格納されているはずの"Hello World!"文字列が、誤って命令として解釈されました。

一方、HELLOシンボル以外の部分は、コード内に.string \"Hellow World!\"を挿入した影響を受けませんでした。

感想

objdumpによる逆アセンブルを妨げるのが元々の目的でしたが、結果としては.textセクション内にデータを挿入するだけで終わってしまいました。

「objdumpを混乱させる」ことに成功したかは微妙ですが、意外と簡単に.textセクション内にデータを挿入できたのは面白かったです。

参考サイト

GCCのインラインアセンブラの書き方 for x86 - OSのようなもの

gcc - Intel assembly syntax OFFSET - Stack Overflow

gccでアセンブラを出力しそれを実行ファイルにする

gdbをデフォルトでintel記法にする | 初期化処理の仕組み - BioErrorLog Tech Blog

X86アセンブラ/x86アセンブラ - Wikibooks

PythonでQEMUとシリアル通信を行う方法の考察

こちらの記事の続きで、QEMUの起動/停止やQEMUモニタの操作と合わせてQEMUとのシリアル通信もPythonで自動化したいと考えています。

そこで、PythonQEMUとシリアル通信を行う方法を考えた結果をメモします。

QEMUからのシリアル通信デバイスの種類

公式ドキュメントによると、シリアル通信のデバイスとして概ね以下の種類が挙げられます。

これらの方法のうち、

  • ptyは割り当てられる仮想端末がQEMUを実行する度に異なる

  • /dev/XXXはroot権限が必要

  • stdioはQEMUモニタで使用する予定

といった理由から、telnetでシリアル通信を行う方法を採用したいと思います。

QEMU側の起動コマンド

qemu-system-aarch64 -cpu cortex-a57 -machine virt -kernel out.img -montor stdio -serial telnet::60000,server,nowait -nographic

telnetの使用するポートはプライベートなものを指定します。

Python側のコード

#!/usr/bin/python3

import telnetlib

host = "localhost"
port = "60000"

tn = telnetlib.Telnet(host=host, port=port)

tn.write(b"foo\r\n")

print(tn.read_until(b"ok\n", timeout=1).decode('ascii'), end="")

数行でtelnetを通してシリアル通信を行うプログラムが書けました。

なお、telnetの出力を読み出す関数としてread_until()以外にもread_some()read_eager()read_lazy()などがありましたが、

  • read_some()は読み出せる文字の数が不安定

  • read_eager()read_lazy()は何故か出力を読み出せなかった(環境の問題?)

といった理由で(少なくとも自分の環境では)使えないので、今回の場合はタイムアウトを指定しつつread_until()を使うのが適切だと思われます。

参考サイト

QEMU Emulator User Documentation

qemu: pyhtonでゲストとシリアル通信 - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ

telnetlib --- Telnet クライアント — Python 3.10.4 ドキュメント

Pythonでtelnet接続してみる | BTY備忘録

x86_64環境上でのARM64組み込みバイナリのクロスコンパイルに関するメモ

Interface 2022年7月号第3部第3章の内容を(今更)実践したので内容をメモします。

なお、誌面ではmacOSを用いて仮想マイコンの実験を行っていますが、本記事ではUbuntu 22.04 LTSを用いて実験します。

ARMツールチェーンのインストール

仮想マイコン上で動作するバイナリを開発する為に、ツールチェーンを公式サイトからダウンロードします。

$ curl -OL https://developer.arm.com/-/media/Files/downloads/gnu/11.2-2022.02/binrel/gcc-arm-11.2-2022.02-aarch64-arm-none-linux-gnueabihf.tar.xz
$ tar Jxfv gcc-arm-11.2-2022.02-x86_64-aarch64-none-elf.tar.xz

/path/to/gcc-arm-xx-x86_64-aarch64-none-elf/binにパスを通し、任意の場所でコマンドが使えるようにして、インストールを終了します。

補足1

  • none: ベアメタル(OSを持たないプラットフォーム)上で動作するバイナリの生成を意味する

OS上で動作するバイナリであれば、標準Cライブラリを使用でき、実行時にはインタプリタによって仮想メモリ上にバイナリが配置されますが、 ベアメタル上で動作するバイナリは、標準Cライブラリを使用できず、実行前に物理メモリ(ROM)上にバイナリが直接配置されます。

補足2

Ubuntuの公式レポジトリで提供されているパッケージ(gcc-arm-none-eabi)では、64bitARMを対象としたプログラムはコンパイルできないようなので、注意が必要です。

ARM64プログラムのクロスコンパイル

下記コマンドでプログラムを生成します。 (引用の範囲を逸脱するため、プログラムのソースコードは省略)

$ aarch64-none-elf-gcc -g -c -static start.S -o start.o
$ aarch64-none-elf-gcc -g -c -static main.c -o main.o
$ aarch64-none-elf-ld -Ttext 0x40080000 -o out.elf start.o main.o
$ aarch64-none-elf-objcopy -O binary out.elf out.img

これらのコマンドについて(自分の分かる範囲で)解説します。

  • aarch64-none-elf-gcc -g -c -static start.S -o start.oaarch64-none-elf-gcc -g -c -static main.c -o main.o

ソースコードアセンブリをオブジェクトファイルへアセンブルするコマンドです。

-staticオプションは、Man page曰く共有ライブラリとのリンクを抑制するとのことです。

直観的には、「OSの無い環境では動的リンカも存在しないので、共有(動的)ライブラリを使わないようにする必要がある」と理解できるのですが、 一方で、「そもそも(少なくとも今回の場合は)共有ライブラリを使用していないので、-staticオプションは不要なのでは」という疑問があります。

実際に、-staticオプションがある場合と無い場合のout.elfファイルにおけるreadelfの出力をdiffしてみたのですが、目立った違いはありませんでした。 (せいぜいファイル名の違いによって.strtabセクションの内容が違っていた程度)

結論としては、少なくとも今回の場合は-staticオプションは不要ではないかと思われるのですが、とりあえず-staticオプションを付けたまま話を続けます。

  • aarch64-none-elf-ld -Ttext 0x40080000 -o out.elf start.o main.o

オブジェクトファイルstart.omain.oをリンクするコマンドです。

-Ttext 0x40080000は、Man Page曰く出力ファイルにおける.textセクションの絶対アドレスを指定するオプションです。

Interfaceの記事曰く、

QEMUではプログラムが0x40080000に配置される

そうなので、プログラムの本体である.textセクションがQEMUにて最初に読み込まれるよう調整するためのオプションだと思われます。

  • aarch64-none-elf-objcopy -O binary out.elf out.img

out.elfout.imgへ変換する処理をしています。 (なぜelfファイルからimgファイルへの変換が必要なのか、そもそもimgファイルが何なのかは現状よく分かってないです…。)

QEMUでバイナリを実行

$ qemu-system-aarch64 -cpu cortex-a57 -machine virt -kernel out.img -monitor stdio

上記コマンドでHello worldが出力されたコンソールが現れれば、誌面の実験の再現には成功です。

参考サイト

Key difference between GCC arm-none-eabi and arm-eabi - Arm Development Studio forum - Support forums - Arm Community

環境

Ubuntu 22.04 LTS