こんにちは!低レイヤの世界へようこそ。
日々、OSの上でスマートなコードを書いているあなたも、「自作のマイコンボード」「まだ誰も動かしたことのないカスタムOS」「ブートローダーの最初の一歩」という泥臭い領域に足を踏み入れると、途端に画面上の便利なIDEやデバッガが沈黙することに気づくはずです。
「市販のGDBserverなんて、この特殊なハードウェアじゃ使えない……」
「printfデバッグをしようにも、UARTすらまだ初期化できていない!」
そんな絶望的な状況を打破し、どんな変態的なハードウェアであってもGDBの強大なデバッグ能力を手なずけるための奥義、それが「GDBカスタムRSP(Remote Serial Protocol)サーバーの自作」です。
これをマスターすれば、宇宙に打ち上げる衛星の制御マイコンから、自作の自作CPUまで、あなたの思い通りにステップ実行できるようになりますよ。それでは、低レイヤの深淵へと一緒に出発しましょう!
—
1. そもそもGDBの「RSP(Remote Serial Protocol)」とは何か?
私たちが普段使っているGDBは、実は極めてスマートな「フロントエンド(UI・脳みそ)」に過ぎません。GDB自体は、直接CPUのレジスタを触ったりメモリを書き換えたりしていません。
では、どうやってデバッグしているのか?
GDBは、デバッグ対象(ターゲット)と通信するために、RSP(Remote Serial Protocol)という非常にシンプルでテキストベースの通信プロトコルを使っています。
[ 開発PC: GDB ] <--- ( TCP / UART / USB ) ---> [ ターゲット: 自作RSPサーバー ] —> [ CPU / ハードウェア ]
つまり、ターゲット側に「GDBからのパケットを受け取り、CPUを操作して、結果をGDBに送り返す小さな通訳(=RSPサーバー)」さえ作ってしまえば、どんな環境でもGDBのフル機能が使えるようになるのです。これが、今回私たちが構築するアーキテクトの魂です。
—
2. 実装の全体像:GDBとRSPサーバーが交わす「合言葉」
RSPは、基本的にはシリアル回線(TCPソケットやUART)を通じて流れるASCII文字のやり取りです。
すべてのパケットは以下のフォーマットに従います。
$[データ本体]#<チェックサム(1バイトの16進数2桁)>
例えば、GDBがターゲットに対して「現在停止している理由は何?」と聞くときは、`?` という文字を包んでこう送ってきます。
$?#3f
これに対し、もしターゲットが「ブレークポイントにヒットして停止したよ(シグナル番号5=SIGTRAP)」と返したいなら、サーバーはこう返答します。
$S05#b8
たったこれだけです!
「えっ、これだけなら自分で書けそうじゃないか?」と思えましたか?その直感が正解です。複雑なGDBの裏側は、実は非常にシンプルなパケットのキャッチボールで成り立っているのです。
—
3. 超最小限!Pythonで動かすカスタムRSPサーバーの構築
今回は、複雑なC言語やアセンブリに飛び込む前に、RSPのプロトコルを完全に理解するために、Pythonを使ったシミュレータ(仮想ベアメタル環境)を構築してみましょう。
このコードを動かすことで、GDBがどのように接続し、どのようなコマンドを投げてくるのかを生々しく観察できます。
ステップ1: 仮想RSPサーバーのコード (`minibox_gdb.py`)
以下のスクリプトを保存してください。これは、TCPポート `1234` で待ち受け、GDBからの接続を迎え撃つ最小限のRSPサーバーです。
import socket
import re
チェックサム(#の後の2桁の16進数)を計算する関数
RSPの仕様では、’$’ から ‘#’ までの文字コードの総和を 256 で割った余りを送ることになっています。
def calculate_checksum(data):
return f”{sum(data.encode()) % 256:02x}”
RSPパケットをラップしてGDBに送信する関数
def send_packet(conn, payload):
checksum = calculate_checksum(payload)
packet = f”${payload}#{checksum}”
conn.sendall(packet.encode())
print(f”[送信] {packet}”)
def run_rsp_server():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 開発PCのポート1234番でGDBからの接続を待ち受けます
server_socket.bind((‘127.0.0.1’, 1234))
server_socket.listen(1)
print(“[INFO] ベアメタル・カスタムRSPサーバーが起動しました。GDBの接続を待っています…”)
conn, addr = server_socket.accept()
print(f”[INFO] GDBが接続してきました: {addr}”)
# 仮想的なCPUのレジスタ(32bitレジスタが16個あると仮定)
# 0~14: 汎用レジスタ, 15: PC (プログラムカウンタ)
registers = [0x00000000] 16
registers[15] = 0x80000000 # ブート時のプログラムカウンタ初期値
while True:
data = conn.recv(1024)
if not data:
break
message = data.decode(‘ascii’, errors=’ignore’)
print(f”[受信] {message}”)
# ACK(確認応答)の返却
# GDBはパケットを送ると、まず単体の ‘+’ を期待します
if ‘+’ in message or ‘$’ in message:
conn.sendall(b’+’)
# パケットの中身を抽出する簡易正規表現
match = re.search(r’\$(.?)\#’, message)
if match:
packet_body = match.group(1)
# 1. ターゲットの拡張機能クエリ (qSupported)
if packet_body.startswith(‘qSupported’):
send_packet(conn, “PacketSize=1024”)
# 2. ターゲットの停止理由問い合わせ (?)
elif packet_body == ‘?’:
# S05: シグナル5(ブレーク/トラップ)で停止中
send_packet(conn, “S05”)
# 3. レジスタ読み出し要求 (g)
elif packet_body == ‘g’:
# 16個のレジスタをそれぞれ8文字の16進数(リトルエンディアン風)に変換して連結
reg_str = “”.join([f”{r:08x}” for r in registers])
send_packet(conn, reg_str)
# 4. スレッド確認 (qC)
elif packet_body == ‘qC’:
send_packet(conn, “QC1”)
# 5. その他の未知のコマンドには空文字(未サポート)を返す
else:
send_packet(conn, “”)
conn.close()
server_socket.close()
if __name__ == ‘__main__’:
run_rsp_server()
ステップ2: サーバーの起動
ターミナルを開き、先ほどのスクリプトを実行します。
$ python3 minibox_gdb.py
[INFO] ベアメタル・カスタムRSPサーバーが起動しました。GDBの接続を待っています…
これで、あなたの自作マイコンボード(のフリをしたPythonプロセス)が、GDBからの接続を待ち構える状態になりました。
—
4. 動作確認:GDBから自作RSPサーバーに接続する
次に、別のターミナルを開き、クロスコンパイル環境用のGDB(今回は汎用の `gdb`)を起動して、先ほどのカスタムサーバーに接続してみましょう。
$ gdb
GDBが起動したら、プロンプトに以下のコマンドを打ち込みます。
(gdb) target remote localhost:1234
【実行結果のイメージ】
GDBの画面と、Pythonサーバーのコンソールを見てください。以下のような通信が裏で行われているはずです。
Python側のログ:
[INFO] GDBが接続してきました: (‘127.0.0.1’, 54321)
[受信] $qSupported:multiprocess+;swbreak+;hwbreak+…#5d
[送信] $PacketSize=1024#9a
[受信] $Hg0#df
[受信] $qC#b4
[送信] $QC1#e5
[受信] $?#3f
[送信] $S05#b8
[受信] $g#67
[送信] $0000000000000000…00000080#cc
GDB側の画面:
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x80000000 in ?? ()
(gdb) info registers
r0 0x0 0
r1 0x0 0
…
pc 0x80000000 0x80000000
おめでとうございます!
独自のRSPプロトコルを介して、GDBが「自作の仮想ターゲット」のレジスタ(PCが `0x80000000` であること)を正しく読み取ることに成功しました。
—
5. 実務への応用:ここから本番のベアメタル環境へ進むには?
このPythonスクリプトの仕組みを理解できれば、これをC言語やRustに書き換え、マイコンのUART(シリアル通信)やJTAG、あるいは独自FPGAのレジスタ経由で動くように移植するだけです。
1. メモリ読み書き (`m`, `M` パケット) の実装
- GDBからの「このアドレスのメモリを読み込め」という命令に対し、マイコンのRAMやフラッシュROMの物理アドレスを直叩きして返すようにします。
2. ステップ実行・ブレークポイント (`s`, `c`, `Z0` パケット) の実装
- CPUのデバッグコア(ARMならCoreSight、RISC-VならDebug Moduleなど)にハードウェアブレークポイントを設定するレジスタ操作を紐付けます。
この仕組みさえ手に入れれば、市販のICE(インサーキット・エミュレーター)が高くて買えない特殊な環境や、誰も仕様を知らない自作CPUのボードであっても、世界最高峰のデバッガーであるGDBの恩恵を100%受けることができるようになります。
「市販のツールがないなら、自分で作ればいい」――このアプローチこそが、ハードウェアとソフトウェアの境界線を自在に行き来できる、真の低レイヤ・アーキテクトの姿です。
毎日の泥臭いデバッグ作業が、少しでもエキサイティングで快適なものになりますように。次の開発も、存楽しんでいきましょう!