← hyperanoka.com

低レイヤ②

/binary2 · 最終更新 2026-09-05 08:05

機械語

基礎知識でELFっていっぱい出てきて、 実行ファイルで、winでいうexeファイルであるということはなんとなくわかったけど、 結局のところ何なのでしょうか? 実行ファイルじゃなくても、x権限さえあれば、.shも.pyも実行できますし、、

javaとかcとかやってると、コンパイルが必要ですよね。 機械が理解できる言葉にしているとよく言いますが、結局のところ具体的に何してるんでしょう コンパイルが必要な言語と不要な言語の違いは? 誰かこの俺に教えてみやがれ!

ソースコード→機械語 人が書いたCのコードはCPUは理解できません。 (他人が書いたコード、、私も理解できないけど、、)

0.作りたいもの→(人間)→ソースコード 1.ソースコード→(コンパイラ)→アセンブリ言語 2.アセンブリ言語→(アセンブラ)→機械語

アセンブリ言語は人間にも読めるそうです。 私は読めません。私は人間ではないのでしょうか。 mov(値をコピー)、add(足し算)、jmp(ジャンプ=分岐)みたいな、短い命令の羅列のようです。

機械語は、本当に01の羅列のようです。 CPUが最終的に実行してるのは、この機械語だけ。のようです。

で、コンパイルやアセンブルは、ファイルの変換や圧縮などと同様に、 デコードやエンコードをしています。 なので、コンパイル、アセンブルもファイル変換の一部ということになりますね。

コンパイルが住んでいると、CPUが解釈できるので、いつでも実行できる。 のですが、 コンパイルがいらない言語(python,ruby,bashとかのインタプリタ言語)は、 コンパイルがされていないわけではなく、(CPUが読めるのは01だけという雑魚なので。) 実行のたびに、1行ずつ解釈して行くので、毎回インタプリタが動くということになりますね。 なので遅い。 ただ、言語の決まりではなく、どう実行するか、の仕組みであり。 pythonにもコンパイルの仕組み自体はあるので、使用者次第でもある。(初知)

ちなみに、JITコンパイルとか呼ばれる中間的なものもあるらしい。(初知)

補足 コンパイラ ソースコードを機械語に変換する作業を1回だけ、事前にやる。

インタプリタ ソースコードを、機械語のファイルに変換して残す、ということを一切しない。 代わりに、実行するたびに毎回、ソースコードのテキストを直接読んで、その場で解釈しながら処理を進める。

リンカ

でかいプログラムはいくつかのファイルに分けることもありますよね。 それはどのように機械語に直されていくのかというと、リンカという仕組みがあるそうです。

ソースコードの時点では、バラバラですよね。 コンパイルされてアセンブリ言語の時点でもバラバラです。 さらに、アセンブルされた機械語の時点でもバラバラです。

この時点ではmainとかも特に意識されていない、ただ部品が散らばっている(未解決の参照)状態です。 ではここから、どのように組み立てていくかというと、 リンカという人が仕事します。

静的リンク 必要な部品(ライブラリのコード)を、全部実行ファイルの中に埋め込んでしまう。 実行ファイルは大きくなるけど、それ単体で完結してて、他に何も要らない。

動的リンク ライブラリのコードは別ファイル(共有ライブラリ)のまま残し、実行ファイルには「実行時にこの名前のライブラリを探して読み込んでください」という参照だけを記録しておく。 実行ファイルは小さく済むし、複数のプログラムがメモリ上で同じライブラリの実体を使い回せる代わりに、実行時にその共有ライブラリが見つからないと起動に失敗する。

で、組み立てはできるんだけど、、一番初めのmainって、どう決まるんだろう? アセンブラもリンカも、「これがmainのファイルだ」という特別な判断は一切してない。そうです。 main()が入ってるオブジェクトファイルも、そうじゃないオブジェクトファイルも、構造としては完全に対等で見分けがつかない。 ファイルからは判断できない。。

実は、、プログラムはmainから始まっていない。 crt0/crt1という小さな「起動処理用の部品」があって、これが本当のエントリポイント(_startという名前が多い)。 この_startが、コマンドライン引数の準備などの下準備をしてから、普通の関数呼び出しとしてmainを呼び出し、mainが戻ってきたら、その戻り値を使ってexitシステムコールを呼ぶ。 crt1からmainという名前の関数を呼び出すのは固定だけど、変えることもできる。

共有ライブラリ

動的リンクされた実行ファイルのELFヘッダには、INTERPという項目で「まずこの動的リンカ(/lib64/ld-linux-x86-64.so.2など)を読み込んでください」という指示が入ってる。 らしい。 実際にプログラムが起動するとき、カーネルはまずこの動的リンカを起動し、その動的リンカが必要な.soファイルを探して読み込み、アドレスを解決してから、やっと本来のプログラムの先頭(エントリポイント)に処理を渡す、という2段構えになってる。 らしい。

コマンドまとめ ※実際に打ったけど、長いから載せないことにしました。

ScanEnuはC言語の自作のペンテストツール(未完成)です 完成したらほしいですか?w

補足 紛らわしい用語(個人的に)

・ファイルのリンク(ハードリンク/シンボリックリンク) 名前」と「すでにディスク上にある実体(inode)」を結びつける操作。

・プログラムのリンカ 「この関数名を呼び出したいけど、実際どこにあるか分からない」という未解決の参照を、「実際にはこのアドレスにありますよ」と結びつける操作。

CPU

CPUがやってることは、実はものすごく単純な繰り返し。 フェッチ(メモリから次にやる命令を1つ取ってくる)→デコード(その命令が何をすべきか解読する)→実行(実際に計算する)。 この3ステップを、1秒間に数十億回のペースでひたすら繰り返してるだけの機械。 このループのことを命令サイクルと呼び、1秒間に何回このループを回せるかがクロック周波数(GHz)。

レジスタ CPU内部にある、ごく少数だが超高速な作業スペース。 メモリよりずっと速いけど、数もサイズも非常に限られてる(x86-64だと汎用レジスタは十数個程度)。

プログラムカウンタ レジスタの中でも特別な1つで、常に「次に読むべき命令のアドレス」を指し続けてる。 if文で処理が分岐したり、ループで同じ場所に戻ったりするのは、実はこのプログラムカウンタの値を書き換えてる。

ALU(Arithmetic Logic Unit) 実際の計算(足し算・引き算・AND/OR/XORのようなビット演算)を担当する回路部分。

マルチコア ここまで話した「レジスタ・ALU・制御の仕組み一式」を、まるごと複数個、1つのチップに詰め込んだものがマルチコアCPU。 「並行と並列」の違いがあるそうで、 コアが1つしかなければ、複数の作業を高速に切り替えて「同時に見せてる」だけ(並行)、 コアが複数あれば、文字通り同時に複数の命令の流れを処理できる(並列)。

コマンド

レジスタ増やせば最強CPUできるんじゃない? と思ったけど、、 そういう問題でもないらしい? 小さいからこそ早い、的な。 そのうちその意味が分かるのだろうか。。?

メモリ(RAM)

メモリは、番号(アドレス)が振られた、たくさんの引き出しのようなもの。 CPUはアドレスを指定して、そこにあるデータを読み書きする。 レジスタよりは遅いけど、ディスクよりは圧倒的に速い、ストレージ階層の中間に位置する存在。

アドレス空間 メモリの1byteごとに振られてる番号の全体の範囲を、「アドレス空間」と呼ぶ。 これはCPUが1つのアドレスを表現するのに何bit使うかで決まる 32bitのCPUなら最大4GBまで、64bitなら理論上はとんでもなく広大な範囲(実際にはハードウェアの都合で制限されてる)まで、アドレスを振れる。

仮想メモリ 現代のOSでは、それぞれのプログラムに対して「自分専用の、まるまる1つのアドレス空間を独占してるかのように見せる」という仕組みが使われてる。 プログラム自身は「自分はアドレス0から始まる、広大な空間を1人占めしてる」と思い込んで動いてるけど、実際の物理メモリ(本物のRAM)上のどこに実体があるかは、OSとCPUが裏で管理してる。 あるプログラムが暴走してメモリをおかしな値で埋めても、仮想アドレス空間が別々に分離されてるおかげで、別のプログラムには基本的に影響が及ばない。

ページング 「仮想アドレス→物理アドレス」の対応付けは、1byteずつじゃなく、「ページ」という単位(多くの場合4KB)でまとめて管理されてる。 この対応表を「ページテーブル」と呼び、CPUの中にあるMMU(メモリ管理ユニット)という専用回路が、OSが用意したこのページテーブルを見ながら、実際のアドレス変換を高速にこなしてる。 1つの仮想ページは、物理メモリ上のどこのページにでも対応付けられる、という間接参照になってるおかげで、「物理的にはバラバラの場所にあるメモリを、プログラムからは連続した1つの空間に見せる」ということが可能になっている。

スワップ 物理メモリが足りなくなると、しばらく使われてないページを、一時的にディスクに退避させる。 退避させた分だけ、物理メモリに空きができて、今使いたいデータのために使える。 退避させたページが再び必要になったら、「ページフォールト」という形でディスクから読み戻す。 このディスクへの読み書きは、レジスタ→メモリの速度差よりもさらに桁違いに遅い。 だから、メモリが足りなくてスワップが頻発すると、PCが急に重くなる。

コマンド

→ASLRというメモリランダム化に気づくかも?

揮発性・不揮発性

揮発性(volatile) 電源を切るとデータが消えてしまう性質。 RAMがこれに当たる。 RAMの中身は、コンデンサに電荷を溜めておく方式(DRAM)で保存されてて、電源が切れると、その電荷はすぐに散って消えてしまう。 それどころか、実は電源が入ってる最中も、この電荷は少しずつ自然に漏れていくので、数ミリ秒おきに中身を読み直して再度書き込み直す(リフレッシュ)という作業を、電源が入ってる間ずっと繰り返してないと、データが保てない。

不揮発性(non-volatile) 電源を切ってもデータが残る性質。 SSDは、NANDフラッシュという方式で、電荷を「浮遊ゲート」という電気的に絶縁された場所に閉じ込めておく仕組みを使ってて、電源がなくてもその電荷が逃げない。 HDDは、円盤(プラッタ)の表面の磁気の向きとしてデータを記録してて、磁気の向きも電源とは無関係に保持され続ける。

速さの話 メモリは早く、SSD,HDDは遅いという話がありますが、、 RAM(DRAM)がコンデンサ1個+トランジスタ1個という、極めてシンプルな構造だからこそ電気的に高速にアクセスできる代わりに、その単純さゆえに電荷がすぐ漏れてしまう。 磁気やフラッシュの浮遊ゲートは、逆に「一度書き込んだら簡単には変化しない」ように意図的に作られてるからこそ不揮発だけど、その分、書き込み・読み込みの操作自体に手間と時間がかかる。

ストレージ階層

キャッシュとRAMがどちらも「揮発性」でひとくくりにされがちだけど、実は内部方式が違う(SRAM vs DRAM)。

コマンド

権限

Linuxでは、すべてのファイルとプロセスに「誰が所有しているか(ユーザー・グループ)」と「誰が何をしていいか(読み・書き・実行)」が紐づいてる。 で、Linuxはすべてをファイルとして扱うんで、実質Linux内の森羅万象に権限があることになる。

ユーザー/グループ 各ユーザーには、人間が読む名前(kaliとか)とは別に、内部的にはUID(数値のユーザーID)が割り当てられてる。 ユーザー名は、/etc/passwdの中で「このUIDにはこの名前」という対応がついてるだけの、いわばラベル。 各ユーザーは1つの主グループに所属してて、さらに複数の補助グループにも同時に所属できる(/etc/groupで管理)。 ファイルやプロセスは、常に「どのUID(所有ユーザー)」「どのGID(所有グループ)」に属してるかを持ってる。

capabilities 「rootは何でもできる」という強力すぎる権限を、約40種類の細かい単位に分割したのがcapabilities。 CAP_NET_BIND_SERVICE(1024番未満のポートを使う権限)、CAP_NET_RAW(生のパケットを扱う権限、pingや自作のスキャンツールが使うやつ)、CAP_SYS_TIME(システム時刻を変更する権限)など。 これのおかげで、「pingのような特定の機能だけにroot相当の力を貸す」ということが、rootを丸ごと渡さずに実現できる。 最小権限の原則(必要な分だけ渡す)を、実際のOSレベルで実現してる仕組み。

見たことないなあ、、

setuid ファイルに付けられる特殊な権限ビット(rwxの4つ目、実行権限の位置にsとして表示される。 例:-rwsr-xr-x)。 これが付いたプログラムは、誰が実行しても、そのプログラムが実行されてる間だけ、ファイルの所有者の権限で動く。 典型例が/usr/bin/passwd。 パスワード変更には、rootしか書き込めない/etc/shadowを書き換える必要がある。 でも一般ユーザーが自分のパスワードを変えられないと困るので、passwdにはsetuidが付いてて、誰が実行してもその瞬間だけroot権限で動作する、という特例が作られてる。 ただしこれは諸刃の剣で、setuidされたプログラムにバグがあれば、それがそのまま一般ユーザーからrootへの権限昇格の入り口になる。 だからこそ、より細かく制御できるcapabilitiesへの移行が進んでる、という経緯につながってる。

コマンド

補足 パーミッションの時に、dだったらディレクトリ、-ならファイルとかある。 で、特殊なのもいくつかある。気になったので調べて一覧にする。

c(キャラクタデバイス)は、mouse1や、/dev/pts/0。 データを1byteずつ、順番にストリームとしてやり取りするタイプのデバイス(キーボード、マウス、端末など)。

b(ブロックデバイス)は、/dev/sdaのような、ディスクそのもの。 決まったサイズの塊(ブロック)単位でランダムアクセスできるデバイスで、こっちはキャラクタデバイスと違って「途中から読む」「まとめて読む」ができる。 この違いが、まさに「ストリームか、ランダムアクセスか」という性質の違いに対応してる。

p(名前付きパイプ)は、mkfifoで作れる特殊なファイルで、2つのプロセスが「ファイルのふりをした通り道」を通じてデータをやり取りする仕組み。

s(ソケット)は、Unixドメインソケットというプロセス間通信用の特殊ファイル。

また、異常とは別で、 末尾に「t」がついてるスティッキービットや、所有者の実行権のところがsになるsetuidがある。 スティッキービットは、これが付いてるディレクトリでは「誰でも書き込みはできるけど、自分が作ったファイルは自分以外削除できない」という特別ルールになる。

補足 名前付きパイプ(FIFO) 普段使うcmd1 | cmd2のパイプは、名前のない、一時的な通り道。 そのコマンドを実行してる間だけ存在して、実行が終われば消える。 名前付きパイプは、これにファイルシステム上の名前をつけて、常設した版。 中に実際のデータが保存されてるわけじゃないからさいずは0。

ソケット 通信の仕組みで、「ネットワーク越しの通信(TCP/IP)」にも「同じマシン内のプロセス間通信」にも使われる、共通の抽象概念。 ls -lでsと表示されるのは、その中でもUnixドメインソケットという、同じマシン内専用のもの。 ネットワークのIPアドレスの代わりに、ファイルシステムのパスを「住所」として使う通信路。 docker.sockへの書き込み権限=事実上rootとなる。

プロセス

プロセスは「実行中のプログラムの1つのインスタンス」。

ELFファイルはただのディスク上の静的なデータだけど、それをexecve()で起動すると、OSがメモリ空間を用意して、 プログラムを読み込んで、PID(プロセスID)を割り振って動かし始める。 これがプロセス。

プロセスが持ってるものは

スレッド プロセスを会社だとすると、スレッドはその会社で働いてる社員。的な。 つまり替えがいくらでも効く定額で壊れるまで使い放題の使い捨て歯車ってこと?(俺)

会社(プロセス)は建物やオフィス備品(メモリ空間、FD)を持っていて、 社員(スレッド)たちはその同じオフィスを共有しながら、 それぞれ別の仕事(命令の流れ、レジスタ、プログラムカウンタ)を進めてる。

1つのプロセスは最低でも1つのスレッド(メインスレッド)を持つ。 マルチスレッドプログラムは、同じプロセスの中に複数のスレッドがいる状態。

メモリ独立/共有

プロセス同士: メモリ空間は完全に独立。 プロセスAがどれだけ変な値をメモリに書いても、プロセスBのメモリには絶対影響しない。 プロセス間で何かデータをやり取りしたいなら、わざわざパイプ・ソケット・共有メモリみたいな明示的な仕組み(IPC)を使う必要がある。

同じプロセス内のスレッド同士: メモリ空間(ヒープ、グローバル変数、開いてるFD)を全部共有する。 ただし、以下の3つだけは各スレッドが専用に持つ。

スレッド間の通信はグローバル変数を読み書きするだけで一瞬でできる反面、 複数のスレッドが同時に同じ変数を書き換えようとすると競合状態になり壊れる。 そこで、ミューテックスとかロックという仕組みで、この変数触ってるから他のスレッドは待つ、みたいになる。 っていうやつがあるらしい。

並行と並列

平行(concurrency): 複数のタスクを「同時に進行してるように見せる」こと。 実はCPUコアが1つしかなくても、OSのスケジューラが数ミリ秒単位でタスクを高速に切り替えることで、人間の目には同時に動いてるように見える。

並列(parallelism): 複数のタスクが物理的に本当に同時に実行されてること。 コアが複数あれば、コアAでタスク1、コアBでタスク2が文字通り同時に進む。

コンテキストスイッチ コアの数より多いプロセス/スレッドを同時に動かしたく見せるための、OSスケジューラの技術。 今動いてるスレッドAを一時停止 → Aのレジスタの値・プログラムカウンタなど「今の状態」を全部メモリに保存(これが「コンテキスト」) → 次に動かすスレッドBの保存しておいたコンテキストをレジスタに復元 → Bの続きから実行再開。これを数ミリ秒~数十ミリ秒ごとに繰り返す。

デーモン 「バックグラウンドで常駐して、特定のイベントやリクエストを待ち続けるプロセス」のこと。 制御端末(tty)を持たず、ps -efで見るとTTY欄が?になっている。 親プロセスがinit/systemd(PID1)になっていることが多く、 元々起動した親から切り離されて「孤児化」させることで、ターミナルを閉じても死なないようにする。 名前の最後にdがつく慣習(sshd, systemd, crond, httpdなど)。

コマンド

補足1 この私は、ps のオプションで、-efをよく使うのですが、

ps auxはBSD形式のオプション記法(ハイフンなし)。 a=他ユーザーのプロセスも含む、u=ユーザー名など詳細表示、x=制御端末を持たないプロセスも含む。 CPU%やMEM%が出るのが特徴。

ps -efはUNIX System V形式のオプション記法(ハイフンあり)。 e=全プロセス、f=フルフォーマット。 PPID(親プロセスID)が出やすいのが特徴で、| grepで絞り込むときに親子関係も一緒に見えるから、デバッグ的に使うには相性がいい。

補足2 ヒープとスタックって、よく聞きますよね、、 といいつつもいつもなんだっけ、ってなるんですね どっちもプロセスの仮想アドレス空間の中にある領域です。使い方が異なります。

スタックは、関数を呼び出すための領域です。 関数を呼ぶたびに、その関数のローカル変数・引数・戻り先アドレスが「スタックフレーム」としてつまれます。=push で、 関数から戻るとそのフレームが剥がされる(pop)。 だから「後入れ先出し(LIFO)」の構造というわけです。 サイズは決まっていて、関数呼び出しが深くなりすぎる(再帰が止まらないなど)と溢れる。 これがいわゆる「スタックオーバーフロー」。

ヒープは、 プログラムが実行中に「動的に」確保するメモリ領域。 C言語で言うmalloc()、C++のnew、Pythonでオブジェクトを作るときなど、コンパイル時にはサイズが決まってないデータはここに置かれる。 自分で確保して自分で解放する必要があって(ガベージコレクションがある言語は自動でやってくれる)、解放し忘れると「メモリリーク」になる。 自分で管理するので、開いたままにしちゃったり、確保の領域を間違えてしまうと大変、、 例えばC言語でmalloc(10)として10バイトの領域を確保したのに、そこに15バイトのデータをmemcpyやstrcpyでコピーしてしまうと、その5バイト分は本来「自分の領域じゃない、隣の誰かの領域」にはみ出して書き込まれる。 はみ出した先が、誤作動を起こしうる、、それがヒープオーバーフロー

スタックは「関数の出入りに合わせて自動で管理される、規則正しい領域」、 ヒープは「プログラマ(や言語のランタイム)が自由に、でも自己責任で管理する領域」。

なので、ヒープはプロセス全体で共有される倉庫的な感じで、 スタックは処理の流れを表す領域。 この違いを覚えておけば、スレッドで間で、ヒープが共有されるけど、スタックは独立してることがすぐにおもいだせる。

補足3 ヒープとスタックの話に似てますが、スレッドで何を共有して何が独立してるかって 単語並べられても知らねえよって感じですよね。 役割で考えましょ。。

共有するもの→ヒープ、グローバル変数、開いているFD、コード自体 これらは「プロセスというアプリ全体が持つデータ・リソース」。

分けるもの→レジスタ、プログラムカウンタ、スタック これらは「今、そのスレッドが実行のどの地点にいるか」という、実行そのものの状態を表す情報。 もしこれを共有してしまったら、スレッドAが「次はこの命令」と思ってる場所と、スレッドBが「次はこの命令」と思ってる場所がごっちゃになって、まともに動けなくなる。

補足4 オーバーフローって狙えるの? はみ出した先のメモリが、どう使われているなんてわかんなくない? と思って調べてみましたが、、 よくわからなかったので、、出直してきます、、 だれかおしえてね、、