低レイヤ①
ファイル
あの、、低レイヤってむずくないですか? PC(だけじゃないけど全部)ってなんで起動するのか、、なんで音が鳴るのか、、 0と1だけの世界とか聞くけど、、にしてはむずくない? ということで低レイヤついて知りたくなっておすすめの本を調べてみたら 「Binary Hacks Rebooted」という本が出てきたので即買って即読んで即あきらめました。 基礎知識があまりにもありません、、なので、、 何とか、書いてあることがわかるくらいの基礎知識を集めたく、、 色々調べて、わからなくなったら戻ってくるためにwikiにしました。。
ファイル
ファイルにも、ネットワークのプロトコルのように、 ヘッダやデータ本体のようなファイルがファイルであるためのルール的なのが存在するようです。 たいていの場合は、以下の4枠だそうです。
- マジックナンバー/シグネチャ
→何のファイルなのかを宣言するところ fileコマンドはここを見ている。
- ヘッダ
→サイズ、メタデータなどデータに関する情報が並んでいる。
- データ
→データの中身。画素データ、音の波形など
- フッターやチェックサム
→終端の合図、ファイルが壊れないないかの検証用文字列など。
※ヘッダには2種類あるそうです。 1.ファイルシステムのメタデータ ファイルシステムとは、ファイルとフォルダという分かりやすい形に整理して見せてる仕組み。
- どのファイルのデータが物理的にディスクのどこに書かれてるか
- 各ファイルのメタデータ(サイズ、権限、タイムスタンプ=inode情報)
- フォルダの中身
を管理している。 inode情報のこと。 サイズ、権限、更新日など。llやstatコマンドで見ている。 ファイルの中身に含まれるわけではなく、ファイルシステムが管理してる「inodeテーブル」という台帳エリアに記載されている。
2.フォーマット内のヘッダ ファイルの先頭に書かれているヘッダ。 ファイルの種類ごとに中身が異なる。 wavなら、周波数、モノラル、ステレオ、とか、 ELFなら、エントリポイント(実行を開始するメモリアドレス)・アーキテクチャ(32/64bit、CPUの種類)・セグメント数とか ファイルの中身として、マジックナンバーの後ろに存在する。
※ELFとは、 Executable and Linkable Formatの略。 Linux/Unix系OSでの実行ファイル・共有ライブラリの標準フォーマット。 Windowsで言う.exeにあたる。 file /bin/lsとかすると、ELF 64-bit LSB pie executableって出るやつ。
実際にそれぞれを見るコマンド
- マジックナンバー:
xxd -l 8 file/file file - ヘッダ(ファイルシステム側):
stat file/ls -l file - ヘッダ(フォーマット内部):
file file - データ本体:
xxd file | less - フッター/チェックサム: フォーマット依存。xxdで見れるから適宜調べる。
statコマンドは、参照された日時もわかる。有用。
また、ファイルに特化したコマンドもあるらしい。 ELF専用
- readelf:ELFファイルのヘッダ・セクション・プログラムヘッダ(セグメント)・シンボルテーブルなどを表示する専用ツール。
- objdump:実行ファイルやオブジェクトファイルを解析するツール。逆アセンブルが得意。
音声動画/画像専用
- ffmpeg:音声・動画を変換/編集するツール。
- ffprobe:音声・動画を調べるツール。コーデックや解像度、サンプルレートなどを表示
- identify:画像ファイルの基本情報(フォーマット、解像度、色深度、ファイルサイズなど)をサッと表示する。
- convert:画像形式変換やリサイズなどを行う。
- compare:2枚の画像を比べて差分を可視化・数値化する。
- exiftool:画像・動画などに埋め込まれたメタデータ(撮影日時、GPS位置情報、カメラ機種など)を読み書きする。
その他:
- cmp:2つのファイルをバイト単位で完全一致か比較。一致してれば何も出力せず、違いがあれば最初に食い違う場所を報告
- steghide:ステガノグラフィ専用ツール。LSBなどを使ってファイルの中にこっそり別データを埋め込む/取り出す。
ファイルの変換は何している?
流れ 変換前ファイルのルールに従ってデコード(0/1に戻す) →変換したいファイルのルールにのっとってエンコード(16進数に戻す?) 前後で情報量が変わらないものの場合は相互変換が可能だが、 そうでなければ不可逆、変換不可などもあり得る。
電子透かしってどうやるの? 画像、音声ファイルとかに、人間に気づかれないように情報を埋め込むやつ。 2種類のやり方がある。 1.最下位ビット方(LSB): ※この辺よくわからなかったから今度勉強しなおして記載
2.周波数領域(DCT/ウェーブレット)方式: ※この辺よくわからなかったから今度勉強しなおして記載
ステガノグラフィと呼ばれるものは使用している技術は同じだが、目的が異なる。
- 「存在自体を隠す秘密の通信」ならステガノグラフィ
- 「誰の物かを追跡・証明する」目的なら電子透かし
steghideっていうツール使ってやるらしい。
ファイルディスクリプタ よく聞く0 = 標準入力(stdin)、1 = 標準出力(stdout)、2 = 標準エラー出力(stderr)とかの話。 プログラムがファイルを操作するとき、ファイルそのものを操作するわけではなく、 番号を使って操作する。 例えば、4番ならファイル書き込み。とか。その番号がファイルディスクリプタ。 どの番号に何の意味を持たせるかはプログラムごとによって異なるが、0-2は共通している。 inodeは「ディスク上のファイルそのもの」を指す恒久的な番号 FDは「今このプロセスがそれを開いてる、一時的な窓口」の番号 普通のファイルでも、ネットワーク通信(ソケット)でも、プロセス間通信(パイプ)でも、/dev/nullのようなデバイスでも、全部「整数番号を渡してread/write/close」という同じインターフェースで扱える。
FD操作して他人のプロンプト見れるんじゃない?キーロガー的な。それ問題では? と思ったけど、権限の問題、ptyをreadできるプロセスは一つなので、奪い合いになり盗み見はならない ので、簡単にはできないらしい。
補足 tty:もともとは物理端末を意味する。 pty:こっちが今は主流。ttyのふりをする。
フォルダ
概要 フォルダ、ディレクトリとは、特殊なファイル扱いだそうですね。 さすがすべてはファイルとして扱うlinuxさん。 普通のファイルは中身がバイト列だが、 ディレクトリの中身はこの名前のエントリは、実体としてこのinode番号にあるという一覧表。 lsはその中身を表示しているだけということになる。
フォルダ関連のコマンド
- ディレクトリの中身一覧: ls -la
- inode番号を見る: ls -id .
- ディレクトリのメタデータ: stat .
- 絶対パスへの解決(シンボリックリンクも辿る): realpath path / readlink -f path
- 今の絶対パス: pwd
- どこが「別のディスク」の継ぎ目になっているか一覧: mount / findmnt
- ディスク使用量とマウントポイント: df -h
- ルート直下の構造をざっと眺める: ls -la /
ファイルを操作するとは
「ファイルを作る/消す/コピーする/移動する」は、 プログラムから見れば全部カーネルにシステムコールでお願いしている。 代表的なのは、open/openat(開く)、read/write(読み書き)、close(閉じる)、unlink(名前を消す=削除)、rename(名前の付け替え)、link(ハードリンクを作る)、symlink(シンボリックリンクを作る)あたり。 rmやcpのようなコマンドは、結局これらのシステムコールを組み合わせて呼んでるだけ。
システムコールについて調べたこと とは、ユーザーモードとカーネルモードをつなぐ唯一の道。 ユーザーモードは権限を制限されていて、CPUに直接触ったり、ディスクを直接読み書きしたり、他のプロセスのメモリを覗いたりできない カーネルモードはハードウェアに直接触れる特権状態。 システムコールは、プログラムが「ファイルを開きたい」「メモリをもっと欲しい」「画面に文字を出したい」みたいな、本来自分ではできないことをやりたいとき、決められた番号と手順に従って「お願い」を出す。(syscallという名前の命令)
じゃあ、rmとかコマンドなんて遠回りせずにシステムコール直接呼べばよくない? と思ったけど、、 結論から言うと、bash自体には「生のシステムコールを直接叩く」ための構文は用意されてない。 そうで、 bashは誰かが作ってくれたcp,rmなどのプログラムを呼び出すだけ。 bashじゃなくて、pythonのlibc.syscallとかならできるぽい。 (それ書くならコマンドのほうが楽だね、、先人様ありがとう)
コピー 元ファイルの中身を先頭から全部読み込んで(read)、新しい場所に全部書き込む(write)。 データ量が多いほど時間がかかるのは全部読んで全部書いてるから。
mv 同じディスク(同じファイルシステム)内での移動なら、実体のデータには一切触らず、ディレクトリの対応表の「名前」の欄を書き換えるだけ(renameというシステムコール1回) 別のディスク/別のファイルシステムをまたぐ移動だと、裏側で「コピーしてから元を削除する」というcp+rmの動きをする
rm unlinkというシステムコールを呼んでる。 「対応表からその名前のエントリを消す」+「inodeのリンク数を1減らす」だけの操作で、実体のデータをその場で消してるわけじゃない。 ほんとに消したいときは、、 HDDの時:shred -v -n 3 -u fileで、これは「そのファイルが物理的に置かれてる場所に、無意味なデータを複数回上書きしてから、最後にunlink(削除)する」というコマンドを実行する SSDの時:ドライブ自体が持ってる「セキュア消去」機能を使う、最初から全体暗号化しておいて、「鍵を捨てる」ことを削除とみなすなど。
一番確実なのは、、、物理的破壊らしいよ、、
ハードリンク 「対応表」に、同じinode番号を指す新しいエントリをもう1本追加する。
シンボリックリンク 新しいinodeを1つ作る。
コピー、リンクの違い コピー: 新しいinodeを1つ作り、データも丸ごと複製する。中身は最初は同じでも、それ以降は完全に独立した別物——片方を編集しても、もう片方には一切影響しない。ディスク容量も2倍消費する。 ハードリンク: inodeは増えない。同じinodeを指す名前が2本になるだけで、データの実体はそもそも1つしかない。だから「オリジナルとリンク」という上下関係すら存在しなくて、どちらの名前で開いても完全に同じ中身を指してる(片方を編集すれば、もう片方から見ても変更後の中身になる)。ディスク容量もほぼ増えない(対応表のエントリ1行分だけ)。 シンボリックリンク: 新しいinodeは作られる(ただし中身はパス文字列だけの小さなもの)。データの複製ではなく「間接参照」なので、元のファイルを消したり移動したりすると、リンク先を見失って壊れる(ハードリンクはinodeを直接共有してるので、名前を1つ消してもリンク数が減るだけで壊れない、という違いにもつながる)。
使い分け: コピーは、元のファイルから、別ファイルを作りたいとき。元ファイルは変えたくないとき。 ハードは、一つのファイルを複数の名前つけたいとき。かつ、オリジナルを消されたら困るとき。(同一ディスク内) シンボリックは、ディスクをまたぎたい、フォルダを指したいなど。
圧縮とは
データにある規則、偏り、繰り返しをより短い表現に変える。 繰り返し出てくるパターンに短い符号を、 あまり出てこないパターンに長い符号を割り当ててサイズを小さくする。
可逆圧縮、不可逆圧縮 可逆圧縮(lossless)は、圧縮後のデータから元のビット列を完全に復元できる方式。zip、gzip、PNG、FLACなど。 あくまで「同じ情報を、より効率的な表現に並べ替えてるだけ」なので、情報量そのものは減らさない。
非可逆圧縮(lossy)は、人間の感覚では気づきにくい情報を意図的に間引いて、二度と戻せない形で捨ててしまう方式。 JPEGやMP3がこれで、「人間にはほぼ聞こえない/見えない成分」から優先的に削っていく。情報そのものを捨てるので、可逆圧縮よりずっと高い圧縮率を出せる代わりに、元には戻せない。
エントロピー 「そのデータがどれだけ予測しにくいか/ランダムに近いか」を数値化したもの。 規則性が強くて予測しやすいデータ(同じ文字の繰り返しとか)はエントロピーが低く、圧縮の余地がたっぷりある。 逆に、完全にランダムなデータはエントロピーが最大で、これ以上圧縮しようがない(どんなに工夫しても、ランダムなものを短く言い換える方法は存在しないから)。 「圧縮できるかどうか」は、結局このエントロピーの高低で決まる、という理解で合ってる。
二重圧縮 一度圧縮したデータは、その圧縮アルゴリズムが「利用できる規則性・偏り」をほぼ使い尽くした後の状態なので、 見た目上ほとんどランダム(エントロピーが最大に近い状態)になっている。 そこにもう一度同じ(あるいは別の)圧縮をかけても、そもそも利用できる規則性がほとんど残ってないので、ほとんど縮まない(むしろヘッダ分だけファイルサイズが増えることすらある)。
ここまで読むと、ファイル変換とほぼ同じだな。。 「圧縮」はファイル変換の特殊ケースといったところかな、、 ファイル変換:「元の形式のルールでデコード→別の形式のルールでエンコード」 圧縮:「元のデータを解釈(デコード)→より短いビット列としてエンコード」 WAVをFLACに変換する操作は、「圧縮」でもあり「フォーマット変換」ともいえる。
補足:エンコードとでコード デジタルって01じゃん?それ単体では何の意味もないらしいよ。 意味を持たせるには、01の羅列をルールに従って解釈する必要がある。 このルールを符号やコードと呼ぶ。 「情報をこのルールに従って01の並びに変換すること」がエンコード、 「その01の並びを、同じルールを使って元の情報に戻すこと」がデコード。 例:エンコード "A" という文字 → (ASCIIというルールに従って) → 01000001
同じ01000001でも、ASCIIというルールなのか、Shift-JISのルールなのかで意味が変わってくる。 このファイルを正しく読むためにどのルールを使うかを示しているのが、マジックナンバー
なぜ電源を押すとつくのか
概要 電源ボタンを押す→電源回路がCPUやメモリ電気が流れる→CPUは固定のメモリアドレスから命令の実行を開始する。 この「どこから始めるか」はCPU/チップセットの設計に焼き込まれてる。らしい。ほんとかよ
ファームウェア(BIOS/UEFI) 4つ答える。
Q1: ファームウェアって何!
ファームウェアは、「特定のハードウェアに専用に組み込まれた、そのハードウェアを直接制御するためのプログラム」のこと。 普通のアプリと違って、ハードウェア自体に内蔵されたチップ(ROM/フラッシュメモリ)に書き込まれていて、OSが起動する前から動いてる。 実はファームウェアはあちこちにある。 SSDのコントローラにもファームウェアり、、キーボードにもマウスにもルーターにも、それぞれ専用のファームウェアが入ってる。 「そのハードウェアを、そのハードウェア自身が最低限動くように制御する、内蔵の小さなプログラム」
CPUは最初に、マザーボード上のチップに書き込まれてるファームウェアを実行します。 POST(Power-On Self Test)という、メモリやCPUなど最低限のハードウェアが生きてるかの自己診断をするそう。 昔ながらのBIOSは16bit時代の古い規格で、扱えるディスク容量に制限(2TBまで)があり、機能もかなり原始的。 今の主流はUEFIで、32/64bit対応、大容量ディスク(GPTというパーティション形式)に対応、ネットワーク越しの起動やグラフィカルな設定画面、Secure Bootにも対応している
役目を終えたファームウェアは、設定された起動順序(ブートオーダー)に従って、次のプログラム=ブートローダに処理を渡す。
ブートローダ ディスクの決まった場所に置かれてる小さなプログラム。 BIOS時代はディスク先頭512byte(MBR)に収まる極小のプログラムが、そこからさらに大きな本体を読み込む2段構え、UEFIでは「EFIシステムパーティション」という専用の領域に、普通のファイルとして.efiという実行ファイル(GRUBなど)が置かれてる形。 ブートローダの仕事は1つで、OS本体(カーネル)をメモリに読み込んで、そこに処理を渡すこと。 起動オプション(カーネルに渡すパラメータ)を選ぶ画面が出ることがあるのも、この段階。 代表的なものがLinuxのGRUB、WindowsのWindows Boot Manager。
カーネル カーネルが起動すると、メモリ管理・最低限必要なデバイスドライバの初期化・ルートファイルシステムのマウントを行い、最初のユーザー空間プロセスを立ち上げる。 initramfsという初期RAMファイルシステムがある、 本当のルートファイルシステム(ディスク上の/)をマウントするには、そのディスクを認識するドライバ(暗号化されてたり、特殊なRAID構成だったりすると特に)が必要になる場合がある。 でもそのドライバ自体が本当のルートファイルシステムの中に入ってたら、卵が先か鶏が先か問題になる。 これを解決するために、カーネルはまずメモリ上に展開される、ごく小さな仮のルートファイルシステム(initramfs)をつかって、 そこに入ってる最低限のドライバでディスクを認識してから、本物のルートファイルシステムに切り替える(switch_root)、という2段階を踏んでる。
先人すげぇ。。
init カーネルが最後に起動する、最初のユーザー空間プロセス。 れが伝統的にinitと呼ばれ、常にプロセスID(PID)は1。今どきの主要ディストリ(Ubuntuも含む)ではsystemdというプログラムがこの役目を担ってる。
PID1は特別な立場で、他のすべてのプロセス(ログイン画面、ネットワーク、各種サービス)を、依存関係を解決しながら順番に立ち上げていく。 あと、親プロセスが死んで孤児になったプロセスを自動的に引き取る役目も持つ。ここから先、サービス群が立ち上がり、ログイン画面、デスクトップへとつながっていく。
initコマンドは、initプロセスに直接シグナルを送っていた。 現在は、ラッパーになっていることが多い。
Secure Boot 起動の各段階(ファームウェア→ブートローダ→カーネル)が、改ざんされていないか電子署名で確認しながら進むUEFIの機能。 ファームウェアの中に「信頼する署名鍵」のリストを持っていて、次に実行しようとしてるプログラムがその鍵で正しく署名されていなければ、実行を拒否する。 OS自体の防御機構がまだ立ち上がっていない、一番無防備な起動の初期段階に、マルウェアやルートキットが入り込むのを防ぐ。 Secure Boot無効にできるからあまり意味ないような。。?w
コマンド
- ls /sys/firmware/efi # 存在すればUEFI起動、存在しなければ従来のBIOS(レガシー)起動
- mokutil --sb-state # Secure Boot が有効か無効か確認
- efibootmgr # ファームウェアが認識してる起動エントリ一覧(要root)
- systemd-analyze # 起動全体の所要時間(ファームウェア/カーネル/ユーザー空間の内訳)
- systemd-analyze blame # どのサービスの起動が遅いか
- systemd-analyze critical-chain # 起動の依存関係チェーン(何が何を待っていたか)
- ps -p 1 -o comm= # PID1の正体を確認(大抵systemd)
- journalctl -b # 今回の起動ログ(カーネルの初期メッセージから追える)
- journalctl -b -1 # 前回の起動ログ(永続ジャーナルが有効な場合)
- lsinitramfs /boot/initrd.img-$(uname -r) | head -30 #initramfsの中身見える
※journalctl -k -bだとdmesgとほぼ同じところを見る。
ななぜOSによって使えるアプリが決まるのか
アンドロイド用のアプリとか、win用のソフトとか、、 めんどくさいよね。。。
理由1 Linuxの実行ファイルはELF形式だが、 WindowsはPE形式、macOSはMach-O形式というものらしく、 OSが実行ファイルを読み込んで起動する部分は、自分のOSの読み方しか知らない。 ファイルの先頭のマジックナンバーがそもそも違うので、Windows用のPEファイルをLinuxに持っていっても、Linuxのローダーは「これはELFじゃない」と最初の数バイトを見た時点で弾いてしまう。
理由2 ABIが違う。 システムコールをするときに、決まった手順や番号などの約束事(ABI)があるのだが、、 それがOSごとに全然違う。 たとえ同じCPU(同じx86-64)で動いてるとしても、Linuxで「システムコール番号2」が意味する操作と、別のOSで「番号2」が意味する操作は、まったくの別物。 だから仮にファイル形式の壁を無理やり突破できたとしても、そのプログラムがOSに送る「お願い」自体が、相手のOSには正しく伝わらない。
ELFヘッダの中身をもう少し細かく見てみる ELFのヘッダ情報は大きく3層構造になってる。
- ELFヘッダ(ファイル先頭): マジックナンバー、32/64bitの区別、エンディアン、対象CPUアーキテクチャ、エントリポイント(実行開始アドレス)、プログラムヘッダ表・セクションヘッダ表がどこにあるか
- プログラムヘッダ(セグメント): OSが実行時に使う情報。「このアドレスからこのアドレスまでをメモリに配置してください、権限はr-x(読取+実行)です」といった、メモリへの配置指示の集まり。動的リンクを使う場合は、「このプログラムを動かすには、まずこの動的リンカ(/lib64/ld-linux-x86-64.so.2など)を先に読み込んでください」という指示(INTERP)もここに入ってる
- セクションヘッダ: リンク・解析時に使う情報。.text(コード本体)、.data(初期値ありのデータ)、.bss(初期値なしのデータ)、.symtab(デバッグ用のシンボル名一覧)など。こっちは実行そのものには必須じゃないので、stripコマンドで削っても普通に動く(デバッグ情報が消えるだけ)
補足① エンディアン 複数byteにまたがる数値を、メモリにどの順番で並べて記録するかのルール。 たとえば0x12345678
リトルエンディアン 下位byteから先に並べる。x86/x86-64はこっち たとえば0x78563412
ビッグエンディアン 上位byteから先に並べる。人間が数字を書く順番と同じ たとえば0x12345678
コマンド
- readelf -h file # ELFヘッダ本体
- readelf -l file # プログラムヘッダ(セグメント) = 実行に必須
- readelf -S file # セクションヘッダ = デバッグ・リンク用、削っても動く
- file binary # 検出された形式を表示
- xxd -l 4 binary # 先頭4byteのマジックナンバーを直接見る(ELFなら7f 45 4c 46)
- ldd binary # 動的リンクしてる依存ライブラリの一覧
- uname -m # 今動いてるマシンのアーキテクチャ確認
んーじゃあ、、 通信プロトコルが進化しているみたいに、win、macのルール全部理解してる hyperlinuxが出てきて、どの実行ファイルでも動かせるとかになる未来あるのか、、?? 仮想マシンとかじゃなくて
第一話完