世界を動かす技術を、日本語で。

D4d4

2025年8月18日原文(nmichaels.org)

概要

  • ARMの d4d4命令 が何か疑問に思った事例の調査
  • LLDリンカ がd4d4を挿入する理由の解析
  • GNU ld との動作比較
  • d4d4命令の アーキテクチャ的意味 の検証
  • 結論として、d4d4は 本来のトラップ命令ではない という指摘

ARMのd4d4命令とリンカの挙動調査

  • ARMの Thumb命令セット で、d4d4が不自然に挿入されている現象の発見

  • LLVMのobjdump によるとd4d4は「相対分岐命令(bmi)」として解釈

  • 該当命令は 常に到達不能領域 に配置されている実例

  • Cコード上は単純な関数リターンのみ、余計な命令は不要なはず

  • 1関数のみの場合にd4d4が出現し、2関数だと消える、3関数で再度出現など 挙動の変化

    • 関数配置やオブジェクトファイルの順序 によってd4d4の位置が変化
  • main.o などのオブジェクト単体にはd4d4は存在せず、 リンク後に挿入 される

  • LLDリンカ がd4d4を使ってオブジェクト境界を 32bitアラインメント していると推測

  • GNU ld は同じ場面で 0x0000(movs r0, r0) を挿入、より無害なパディング

LLDのd4d4挿入理由と由来

  • LLVM lldのソースコード (ARM.cpp)にて、trapInstr = {0xd4, 0xd4, 0xd4, 0xd4}と明記
  • コミットログ によると、Theo de Raadtの提案で「トラップ命令」としてd4d4を推奨
  • x86系の0xCC(int3)と同様の「穴埋めトラップ命令」目的で導入
  • 公式ML等に明確な根拠は見当たらず、設計者の判断に依存

d4d4命令の実際の意味と問題点

  • ARMv7-Mアーキテクチャリファレンスマニュアル にてd4d4のデコードを確認
  • d4は 0b11010100、Thumbの16bit命令として解釈
  • 0b1101xxは 条件付き分岐命令 (B命令)に該当
  • d4d4は「bmi -0x58」(条件付き相対分岐)にデコードされる
  • UDF(未定義命令)SVC(スーパーバイザコール) ではない
  • トラップ命令 としての本来の役割(CPU停止)を果たさず、分岐命令でしかない
  • 条件が揃えば コードの他領域へジャンプ する可能性があるため安全性に疑問

結論と提言

  • objdumpの解析は正しい、d4d4は分岐命令
  • LLDが「トラップ命令」としてd4d4を選んだのは 設計ミスの可能性
  • 本来は未定義命令(UDF)NOP 等の方が安全
  • LLDの現仕様では、 意図しない分岐 が発生するリスク
  • GNU ld のように無害な値(0x0000等)を使う方が望ましい
  • バグ報告を推奨、今後の修正が期待される

まとめ

  • ARM/Thumb環境で LLDリンカが挿入するd4d4命令 は「トラップ」ではなく「条件付き分岐」
  • 安全性や意図に疑問 が残る実装
  • ARM向けリンカの パディング命令選択 には注意が必要

Hackerたちの意見

神よ、こういう使い方のために責任を負うのが大好きだ。

これって悪用される可能性ある?

もし実行の流れを十分にひっくり返して、行くべきじゃない場所にジャンプできるなら、条件分岐よりもバイナリ内で他にもっと良いターゲットがあると思うよ。

要するに、リンカーが4d3d3d3を引き起こしてるってことね。

もしその命令が実行されることになったら、ひどいハイゼンバグが発生しそうだね?

あなたは難読化されたサプライチェーンの国家アクターの脆弱性が欲しいの?これがその方法だよ!(誰かが徹夜でバグを探さない限りは...)

これ、クールだね!それに、コミットにはトラップ命令とINT3(デバッグ)がx86用に使われてるって名前がはっきりとバグだって示してる。これを見てここまで来るとは思わなかったな。すぐにパディング用のセンチネル値だと勘違いして、次に進んでたと思う。いい仕事だね。

面白い話だね!ちょっと脱線して愚痴を言わせてもらうと、こういうことを書く人がいるのが信じられない。> [このパッチ]は実行可能セクションの穴を0xd4(ARM)や0xef(MIPS)で埋める。これらのトラップ命令はTheo de Raadtが提案した。 でも、コードには書かずにコミットメッセージに入れるって、何で?コストは?Cファイルの定数の上に2~3行のコメントがあることのデメリットは何?明らかにそうじゃないのに、全てが超明白であるかのように振る舞う理由は何?特にFOSSの世界では、全知のオラクルのようにコードを書くべきだという暗黙の文化的ルールがあるみたいで、コメントは負け犬のためのもの(説明的な変数名もね)。どうしてそんな風になったのか、全く理解できないよ。

多くの開発者は、コードは自己文書化されるべきだと思ってるけど、俺もその意見には賛成だよ。でも残念ながら、実際に自己文書化されたプロジェクトに関わったことはないな。リーダーたちがそう望んでたのにね。

コミットには文脈を入れるのが好きなんだ、コードのコメントじゃなくてね。理由は簡単で、コメントはチェックされないから。例えば「これはジョン・ドーが提案したからこうなってる、こっちの方が効率的だよ」って書いても、他の誰かがコードをバグだらけにしたり、間違ったり、遅くしたりすることがある。そうなると、そのコメントはもう存在しない動作を説明していて、コードが何かを意味していると誤解させることになるんだ。もう一つの理由は、コメントがノイズになるってこと。無駄なコメントが増えれば増えるほど、コア開発者たち(ほとんどのコードを書いて読んでる人たち)はコメントを無視するようになる。そうすると、「XYZがまだ初期化されてないときにこれを呼ばないように気をつけて、ABCが起こるのが気にならないなら」みたいな重要なコメントも無視されちゃって、結局コメントは役に立たなくなる。コミットメッセージは特定の変更に付随してるから、もしコードの行がどうしてそうなってるのか知りたいなら、git blameを使ってどのコミットが原因か、問題番号、著者、レビューア、文脈、履歴なんかを確認できる。もしこのパッチを簡単に説明するコメントが必要なら、多分必要だね。でもコミットメッセージには他の文脈を加えるべきだと思う。

でも、これは意図通りに動いてるの? コードはドキュメントでごちゃごちゃしてないし、コードを読むときに必ずしも意味があるわけじゃないけど、コミットを読むことでコードがこう書かれた理由がわかるんだ。

え? 記事からもう少し引用すると: > [W]e find this in ARM.cpp: > trapInstr = {0xd4, 0xd4, 0xd4, 0xd4}; トラップ命令がパディングとして使われていることは説明する必要があるけど、ここからはそれが明らかかどうかはわからないね。実際のコードを開くと[1]、trapInstrの出現はすべて次のように続いている: > void ARM::writePlt( /* ... / ) { > / ... */ > memcpy(buf + 12, trapInstr.data(), 4); // 16バイトの境界にパディングする、これは絶対的にベストではないけど、十分に明確だと思う(もちろんPLTが何かを知っていればね、リンクを作る人なら知ってるべきだ)。伝統的にNOPを使うオプションがASLRを効果的でなくするから、(意図されている)トラップを使っていることを説明する価値があると思う。でも、あなたが引用してるコミットメッセージにはそれも書いてないね。[1] https://github.com/llvm/llvm-project/blob/b20c291baec94ba370...

Hacker Newsで議論の続きを見る