読書メモ~Linuxのしくみ(更新中)
概要
「Linuxのしくみ」を読んで大事そうなところをメモ
コンピューターシステムの階層
階層
ユーザープログラム
OS外ライブラリ
OSライブラリ
カーネル
ハードウェア
- 実際にはこんなに綺麗に階層化されてない
用語
プログラム
- Go言語などのコンパイラが多言語だと、ビルドした後の実行ファイルがプログラム
- Pythonとかのスクリプト言語だと、ソースコードそのものがプログラム
- カーネルもプログラムの一種。マシンの電源を入れるとカーネルが起動し、それ以外のプログラムはすべてカーネルの後に起動する
- プログラムとは、コンピュータ上で動作する一連の命令およびデータをひとまとめにしたもの
プロセス
- 動作中のプログラムのことをプロセスと呼ぶ
- 動作中のプロセスのことをプログラムと呼ぶこともあるので、プログラムの方がより広義な概念
カーネル
- プロセスからデバイスに直接アクセスさせたくない
- ハードウェア(CPU)にはモードという機能がある
- カーネルモードとユーザモードがある
- カーネルモードであればCPUは制限をかけない
- ユーザモードであれば、CPUは特定の命令を実行できないようにする
- Linuxの場合、カーネルのみがカーネルモードで動作し、デバイスにアクセスできる
- プロセスはユーザモードで動作するため、デバイスにアクセスできない
- カーネルは、プロセスが共有するリソースを一元管理して、プロセスに配分している
システムコール
- プロセスからカーネルへの処理の依頼
- 新規プロセス生成やハードウェア操作などの処理を依頼する際にシステムコールを発行する
- プロセスがカーネルに対してシステムコールを発行すると、CPUで「例外」というイベントが発生し、CPUのモードがユーザモードからカーネルモードに遷移する。そして、カーネルが処理を終えたら、再びユーザモードに戻る
標準Cライブラリ
- Linuxでも標準Cライブラリが提供されている(libc)
- C言語で書かれたプログラムはほとんどすべてlibcをリンクしている
- bashやecho、Pythonも標準ライブラリをリンクしている
- lss
libc.so.6が標準Cライブラリを差す
$ ldd /bin/bash
linux-vdso.so.1 (0x00007ffd3afbb000)
libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007efda6879000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007efda6873000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efda6681000)
/lib64/ld-linux-x86-64.so.2 (0x00007efda69e0000)
$ ldd /bin/echo
linux-vdso.so.1 (0x00007ffcc7529000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fba60498000)
/lib64/ld-linux-x86-64.so.2 (0x00007fba606a0000)
$ ldd /usr/bin/python3
linux-vdso.so.1 (0x00007fff9bcdb000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f99cd902000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f99cd8df000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f99cd8d9000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f99cd8d4000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f99cd785000)
libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f99cd757000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f99cd739000)
/lib64/ld-linux-x86-64.so.2 (0x00007f99cdaff000)
システムコールのラッパー
X86_64X86_64アーキテクチャのCPUだと、親のプロセスIDを取得するシステムコールは次のアセンブリプリコードで発行される
mov $0x6e,%eax
syscall
一方、arm64アーキテクチャでは、次のようになる
mov x8, <システムコール番号>
svc #0
C言語ではラッパー関数 getppid() が用意されており、これを使って親のプロセスIDを取得できる
pause()もラッパー関数である
ラッパー関数により、わざわざアセンブリコードを書く必要がない
CPUアーキテクチャ
X86
米インテル(Intel)社がパソコンなど向けに開発・製造しているマイクロプロセッサ(MPU/CPU)製品のシリーズ名。また、同シリーズのプロセッサを動作させるための命令語の体系(命令セットアーキテクチャ)の名称。1978年に開発された16ビットMPU「8086」を始祖とするためこのように呼ばれる。
AMD64 (x86_64)
https://e-words.jp/w/AMD64.html
米AMD社が開発したマイクロプロセッサ(CPU/MPU)の64ビット命令セットで、32ビットのx86系命令セットとの互換性を維持したまま64ビットコードの実行を可能にしたもの。
Intel64
米インテル(Intel)社がx86系マイクロプロセッサ(CPU/MPU)製品に組み込んだ64ビット命令セットで、32ビットのx86系命令セットとの互換性を維持したまま64ビットコードの実行を可能にしたもの。米AMD社のAMD64(x86-64)とほぼ同じもの。
ARM64
https://e-words.jp/w/ARM64.html
英アーム(Arm)社が開発したマイクロプロセッサ(MPU/CPU)の基本設計(アーキテクチャ)の一つで、プログラムやデータを64ビット単位で処理するためのもの。
静的ライブラリと共有ライブラリ
プログラム生成の流れ
-
オブジェクトファイルが使うライブラリをリンクし、実行ファイルを作る
-
静的ライブラリ
- リンク時に、ライブラリ内の関数をプログラムに埋め込む
-
共有ライブラリ
- リンク時に、ライブラリの関数の参照先を実行ファイルに埋め込む
- プログラムの実行中にライブラリをメモリにロードして、ライブラリの関数を利用する
以下のようにpause.cというコードを用意する。
#include <unistd.h>
int main(void) {
pause(5);
return 0;
}
静的リンクする場合は次のようにコンパイルする
これでpauseという名前の実行ファイルが作成される
cc -static -o pause pause.c
ls -lやlddでサイズ・リンク状態を確認する。
一方、共有ライブラリをリンクさせる場合は次のようにコンパイルする
cc -o pause pause.c
プロセスを分裂させるfork()関数
- fork()関数はプロセスを分裂させるシステムコールのラッパー関数
- 親プロセスのメモリを子プロセス用にコピーを作成する
- fork()関数の戻り値は、親プロセスからの戻り値は子プロセスのIDで、子プロセスからの戻り値は0である
- 戻り値を利用して、処理を分岐させることができる
#!/usr/bin/python3
import os, sys
ret = os.fork()
if ret == 0:
print("子プロセス: pid{}, 親プロセスのpid={}".format(os.getpid(), os.getppid()))
exit()
elif ret > 0:
print("親プロセス: pit={}, 子プロセスのpid={}".format(os.getpid(), ret))
exit()
sys.exit(1)
- 実行すると次のように親プロセスと子プロセス両方にPIDが表示される
$ ./fork.py
親プロセス: pit=355, 子プロセスのpid=356
子プロセス: pid356, 親プロセスのpid=355
別のプログラムを起動するexecve()関数
- execve()関数は現在のプロセスのメモリを、実行ファイルから読みだした内容で書き換える
- それによって、新プロセス用のプロセスメモリが作成され、新プロセスを開始できる
- fork()関数と組み合わせることで親プロセスから新しいプロセスを生成できる
#!/usr/bin/python3
import os, sys
ret = os.fork()
if ret == 0:
print("子プロセス: pid={}, 親プロセスのpid={}".format(os.getpid(), os.getppid()))
os.execve("/bin/echo", ["echo", "子プロセス pid={} からこんにちは".format(os.getpid())], {})
exit()
elif ret > 0:
print("親プロセス: pit={}, 子プロセスのpid={}".format(os.getpid(), ret))
exit()
sys.exit(1)
$ ./fork-and-exec.py
親プロセス: pit=387, 子プロセスのpid=388
子プロセス: pid=388, 親プロセスのpid=387
子プロセス pid=388 からこんにちは
initプロセス起動までの流れ
- コンピュータ電源オン
- BIOS、UEFIなどのファームウェアが起動してハードウェア初期化
- ファームウェアがGRUBなどのブートローダを起動
- ブートローダがOSカーネル(Linuxカーネル)を起動
- Linxカーネルがinitプロセスを起動
- initプロセスが子プロセスを起動していく
↓ initプロセスが子プロセスを起動した様子
$ pstree -p
init(1)─┬─init(13)─┬─init(14)───bash(15)───sudo(303)───unshare(304)───bash(305)
│ └─init(84)
├─init(320)───init(321)───bash(322)───pstree(390)
└─{init}(6)
プロセスの状態
- 実行可能状態:CPU実行権がない状態
- 実行状態:CPU実行権を得た状態
- スリープ状態:アイドルプロセス動作している状態。イベントが発生すると、実行可能状態に遷移
- ゾンビ状態
- プロセス終了
プロセスの終了
exit_group()というシステムコールでプロセスを終了できる
このシステムコールの中で、カーネルがメモリなどのプロセスのリソースを回収する
プロセス終了後に、wait()やwaitpid()というシステムコールを呼び出すことで、プロセスの戻り値、シグナルによって終了したか否か、終了までにどれだけのCPU時間を使ったかを確認できる。
wait()システムコールを呼ぶことで上記情報を得られるということは、プロセス終了後もなんらかの形で終了した子プロセスがシステム上に存在していることを意味する
オフセット、サイズ、メモリマップ開始アドレス、エントリポイント
実行ファイルでは、プログラムのコードやデータに加えて、以下を保持している
- コード領域のファイル上オフセット、サイズ、メモリマップ開始アドレス
- データ領域のファイル上オフセット、サイズ、メモリマップ開始アドレス
- エントリポイント(最初に実行する命令のメモリアドレス)
ファイル上オフセット、サイズ、メモリマップ開始アドレスはreadelf -S <実行ファイル名>で確認できる。
エントリポイントはreadelf -h <実行ファイル名>で確認できる。
$ cc -o pause -no-pie pause.c
$ readelf -S pause
There are 31 section headers, starting at offset 0x3938:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000400338 00000338
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.build-i NOTE 0000000000400358 00000358
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000040037c 0000037c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000004003a0 000003a0
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000004003c0 000003c0
0000000000000060 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000400420 00000420
000000000000003e 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 000000000040045e 0000045e
0000000000000008 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000400468 00000468
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000400488 00000488
0000000000000030 0000000000000018 A 6 0 8
[11] .rela.plt RELA 00000000004004b8 000004b8
0000000000000018 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000401000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000401020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[14] .plt.sec PROGBITS 0000000000401040 00001040
0000000000000010 0000000000000010 AX 0 0 16
[15] .text PROGBITS 0000000000401050 00001050
0000000000000175 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 00000000004011c8 000011c8
000000000000000d 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000402000 00002000
0000000000000004 0000000000000004 AM 0 0 4
[18] .eh_frame_hdr PROGBITS 0000000000402004 00002004
0000000000000044 0000000000000000 A 0 0 4
[19] .eh_frame PROGBITS 0000000000402048 00002048
0000000000000100 0000000000000000 A 0 0 8
[20] .init_array INIT_ARRAY 0000000000403e10 00002e10
0000000000000008 0000000000000008 WA 0 0 8
[21] .fini_array FINI_ARRAY 0000000000403e18 00002e18
0000000000000008 0000000000000008 WA 0 0 8
[22] .dynamic DYNAMIC 0000000000403e20 00002e20
00000000000001d0 0000000000000010 WA 7 0 8
[23] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000404000 00003000
0000000000000020 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000404020 00003020
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000404030 00003030
0000000000000008 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00003030
000000000000002b 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 00003060
00000000000005e8 0000000000000018 29 45 8
[29] .strtab STRTAB 0000000000000000 00003648
00000000000001ca 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 00003812
000000000000011f 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
$ readelf -h pause
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401050
Start of program headers: 64 (bytes into file)
Start of section headers: 14648 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
エントリポイントの値以下の通り。
Entry point address: 0x401050
namespace
システムに存在する様々な種類のリソースに対してそれぞれnamespaceが存在する
あるnamespaceに所属するプロセスに見かけ上は独立したリソースを見せる機能
例えば以下の種類がある。
pid namespace
以下の例では、pid nsが4026532192である。
$ ls -l /proc/$$/ns/pid
lrwxrwxrwx 1 bluen bluen 0 Mar 16 23:21 /proc/15/ns/pid -> 'pid:[4026532192]'
unshare --pidコマンドで、pid nsを新規作成しbashを新規作成したnamespace上で実行する。
pid nsが4026532211になっており、親pid nsとIDが異なっていることがわかる。
さらに、子pid nsからは、親pid nsのプロセスが見えないことがわかる。
$ sudo unshare --fork --pid --mount-proc bash
$ echo $$
1
$ ls -l /proc/1/ns/pid
lrwxrwxrwx 1 root root 0 Mar 16 23:23 /proc/1/ns/pid -> 'pid:[4026532211]'
$ ps ax
PID TTY STAT TIME COMMAND
1 pts/0 S 0:00 bash
9 pts/0 R+ 0:00 ps ax
別の端末をひらいて、親pid nsから確認すると、子pid nsに属するbash (PID=305)は確認できる。
つまり、親pid nsからは子pid nsのプロセスが確認できる。
さらに親pid nsで確認できるbashのプロセスID (PID=305)と子pid ns内で確認できるプロセスID (PID=1)が異なる点にも注意。
$ pstree -p
init(1)─┬─init(13)─┬─init(14)───bash(15)───sudo(303)───unshare(304)───bash(305)
│ └─init(84)
├─init(320)───init(321)───bash(322)───pstree(387)
├─{init}(6)
└─{init}(386)
Linuxカーネルで利用できるnamespaceが増えていっている。
コンテナ
コンテナは前述のnamespaceを利用して、他のプロセスとは実行環境がわかれているプロセスのこと。
どのnamespace (pid ns, user ns, mount ns)を分離するかは、コンテナランタイムによって異なる。
他のプロセスと実行環境が分かれているため、ホストOSや他のコンテナに起因する問題があった場合には、コンテナの中からではわからないことに注意が必要。
さまざまなコンテナランタイム
コンテナの場合、ホストとなるシステムとホスト上の全コンテナがカーネルを共有する
カーネルに脆弱性がある場合、コンテナのユーザによってホストOSや他のコンテナの情報を盗み見られるリスクがある
Docker以外に、さまざまなコンテナランタイムが生まれている
- Kata Container
- gVisor