人生は勉強ブログ

https://github.com/dooooooooinggggg

ThinkPad X13 Gen 1 (AMD) + Ubuntu 20.04におけるハードウェア周りの問題の解消

環境

ThinkPad X13 Gen 1

  • AMD Ryzen 7 PRO 4750U (1.70GHz, 8MB)
  • 32GB DDR4 3200MHz
  • 1TB Solid State Drive, M.2 2280, PCIe-NVMe, OPAL, TLC
  • 13.3型FHD液晶 (1920x1080) IPS、光沢なし、300nit、マルチタッチ非対応、狭額縁ベゼル、72%NTSC

デスクトップ環境はKDE Plasma

画面の明るさを調整できない

  • ディスプレイの明るさを調整できない
  • 外部モニタに切り替えることができない

といった問題が発生した.これに付随して発生した

このような制約も,以下のドライバを入手することで解消可能であった。 このツイートにあるように、sddmにした際に起動しなくなる問題も解決した。 ちなみに、この起動できない状態、見かけ上は起動できていないがSSHでログインすることは可能であった。

カーネルバージョンを5.7以上にあげることが解決策*1として示されているが、7/12に出た以下のドライバをインストールすることで解消可能。

Radeon™ Software for Linux® 20.20 Release Notes | AMD

www.amd.com

定期的にフリーズ(マウスは動く)

Ubuntu日本語フォーラム / ubuntu18.04.2 フリーズ回避法 マウスは動く場合にて一定の解決策が示されている。なおまだ試していない。けっこううざいのであとで解決したら追記予定。

2020/10/9 追記

AMD Ryzen 向け一部マザーボードでは UEFI から C6 ステートを disabled にできない件を参考に以下のスクリプトを取得、面倒くさいが毎回起動時に実行することでc6 stateをdisableにしてみる。

cd 任意のパス
git clone https://github.com/r4m0n/ZenStates-Linux.git
cd ZenStates-Linux
sudo python zenstates.py -l
# P0 - Enabled - FID = 66 - DID = C - VID = 35 - Ratio = 17.00 - vCore = 1.21875
# P1 - Enabled - FID = 60 - DID = C - VID = 60 - Ratio = 16.00 - vCore = 0.95000
# P2 - Enabled - FID = 62 - DID = E - VID = 66 - Ratio = 14.00 - vCore = 0.91250
# P3 - Disabled
# P4 - Disabled
# P5 - Disabled
# P6 - Disabled
# P7 - Disabled
# C6 State - Package - Enable
# C6 State - Core - Enable

以下を起動時に実行

sudo python zenstates.py --c6-disable

確認

sudo python zenstates.py -l
# P0 - Enabled - FID = 66 - DID = C - VID = 35 - Ratio = 17.00 - vCore = 1.21875
# P1 - Enabled - FID = 60 - DID = C - VID = 60 - Ratio = 16.00 - vCore = 0.95000
# P2 - Enabled - FID = 62 - DID = E - VID = 66 - Ratio = 14.00 - vCore = 0.91250
# P3 - Disabled
# P4 - Disabled
# P5 - Disabled
# P6 - Disabled
# P7 - Disabled
# C6 State - Package - Disabled <- 変わっている
# C6 State - Core - Disabled <- 変わっている

PCを閉じたあとに復帰できない

ThinkPad X13 Gen 1 (AMD) + Ubuntu 20.04 における不具合の解決 - Qiitaに示されているように、BIOSの設定を変更することで解決可能。

ー>Config > Power > Sleep StateWindows 10からLinuxに変更。

参考

  1. Issues!!! Ryzen 4000 (4800U) Compatibility - Ubuntu 20.04 LTS : linuxhardware
  2. Ubuntu日本語フォーラム / ubuntu18.04.2 フリーズ回避法 マウスは動く場合
  3. ThinkPad X13 Gen 1 (AMD) + Ubuntu 20.04 における不具合の解決 - Qiita
  4. Radeon™ Software for Linux® 20.20 Release Notes | AMD
  5. AMD Ryzen 向け一部マザーボードでは UEFI から C6 ステートを disabled にできない件

www.reddit.com

forums.ubuntulinux.jp

qiita.com

Linux Kernelをビルドする際,プリプロセッサの出力を残す

この記事はSFC-RG Advent Calendar 2019の17日目の記事です.

結論

Makefileこの辺KBUILD_CFLAGS += -save-temps=objという行を加えてビルドする.

before

KBUILD_CFLAGS   := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
           -fno-strict-aliasing -fno-common -fshort-wchar \
           -Werror-implicit-function-declaration \
           -Wno-format-security \
           -std=gnu89
KBUILD_CPPFLAGS := -D__KERNEL__

after

KBUILD_CFLAGS   := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
           -fno-strict-aliasing -fno-common -fshort-wchar \
           -Werror-implicit-function-declaration \
           -Wno-format-security \
           -std=gnu89
KBUILD_CFLAGS += -save-temps=obj
KBUILD_CPPFLAGS := -D__KERNEL__

プリプロセッサ

C言語などでプログラムを書いた時,何も考えない場合は

gcc -o hello hello.c

などのようにしたコンパイルを行う. 少し本格的なプログラムや,実行環境などによって分岐をしたい場合は,マクロを使用することになる.

コンパイルにはいくつかの工程があり,その工程の序盤に,ソースコードに対して前処理を行うプログラムのことをプリプロセッサ(preprocessor)と呼ぶ.

プリプロセッサ - Wikipediaにあるように,

ファイルの読み込み (including) マクロの展開(シンボルを、あらかじめ定義された規則に従って置換する) コンパイル条件によるソースコードの部分的選択 コメントの削除

を行う. これらの工程を行うのがC言語におけるgccプリプロセッサである.

これらの情報が削除できる理由として,例えば,コメントは,開発者のためにあるものであって,コンパイラ的には何の意味もない. 同様に,マクロなどもビルド時には値が決定しているし,includeなどを使ったファイルの読み込みも,開発者が同じコードをいくつも書きたくないからファイルとして分割しているのであって,コンパイラからしたら,全て一つのファイルにまとめておいてほしい.

この,プリプロセッサが行う前処理の結果は,通常一時的なものであって,コンパイル後にファイルが残ることはない. しかし,マクロの挙動を見るなどの目的でプリプロセッサの出力を見たい場合のオプションとして,Eオプションがgccには用意されている.

-E' オプションを使用すると、GCC はプリプロセス以外の処理を行いません。 以下に示すオプションのうちのいくつかは、-E' と同時に使用された時のみ意味をもちます。なぜならば、これらのオプション によって、実際のコンパイルには不適当なプリプロセッサ出力が生成されるためです。

Linux Kernelのマクロを展開したい

一方で,Linux Kernelのような複雑なビルド手順がMakefileにまとまっている.しかし,そのビルド手順は複雑きわまりなく,単にgccを付け足すだけでは厳しいのは明白である.意味がわからない.

カーネルコンフィグを元にマクロを展開しようにも,マクロの数が多すぎて,とても手作業では厳しい.そこで,今回はgccのオプションを使って実現する.

Linux Kernelのビルド手順に関しては,過去記事で触れたが,今回の記事でも簡単に書く.

blog.ishikawa.tech

blog.ishikawa.tech

  1. 何かしらの方法で.configを生成する.
  2. 以下のように,Makefileこの辺KBUILD_CFLAGS += -save-temps=objという行を加える
  3. makeする
KBUILD_CFLAGS   := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
           -fno-strict-aliasing -fno-common -fshort-wchar \
           -Werror-implicit-function-declaration \
           -Wno-format-security \
           -std=gnu89
KBUILD_CFLAGS += -save-temps=obj
KBUILD_CPPFLAGS := -D__KERNEL__
make -j9

少し待つと,ビルドが完了するが,ビルドが完了したディレクトリでls -aコマンドなどを叩いてみると,拡張子にiを持ったファイルが生成されているのがわかる.

最後に,vscodeなどを用いて文字列の探索を行う.マシンパワーに頼る.

例えばtask_structの最終的な型を知りたい場合は,struct task_struct {みたいな文字列で検索してみる. すると,sched.hをincludeしている大量の痕跡が見つかる.よく見てみると,マクロも全て展開されている.

以下はMan page of GCCにある説明である.

-save-temps
    通常の ``一時'' 中間ファイルを消去せずに保存します。これらは カレントディレクトリに置かれ、ソースファイルに基づいた名前が付けられます。 従って、`foo.c' を `-c -save-temps' を使用してコンパイルした場合は、 `foo.cpp', `foo.s' が、`foo.o' と同様に生成されます。

まとめ

本当はもっといい方法があるのだと思うが,これしか思いつかなかった.

参考

KASLRの実装と挙動の確認

この記事はSFC-RG Advent Calendar 2019の16日目の記事です. 今年も遅くなって申し訳ないです.

この記事では,KASLRkernel address space layout randomizationについて記述する.

KASLRは,カーネル領域における仮想アドレス空間のランダム化である.この基本的な仕組みは,カーネルに限らずASLRという仕組みで実現されているため,まずは,ASLRの目的や仕組みついて記述する.

ASLR

ASLRに関しては,こちらのQiitaの記事にて概要が書かれている. (ちなみにKASLRについても触れられている)

ここで述べられているシチュエーションは,

不正な方法によってプログラムに特定の命令を実行させる、不正なデータを操作させるというものです。攻撃には、(当然ながら)攻撃に使う命令、あるいはデータのアドレスが必要になります。ASLRが無い環境においてはプログラムのコードやデータは固定されたアドレスにロードされるので、動かしているプログラムのバイナリがどんなものかわかっていれば、攻撃者が攻撃に使うコードやデータのアドレスを知るのは簡単です。

となっている.実行ファイル内のコードセグメントと呼ばれる領域にはプログラムの命令自身が格納されている.そのほかにもスタック領域やヒープ領域と呼ばれる領域も存在する. ASLRが無効な状態で実行可能ファイルを実行すると,これらの領域は常に決まった仮想アドレス空間にロードされる.

すると,仮想アドレスの推測が攻撃者にとって容易になってしまう.そこで登場したのが,ASLRである.この機能を使用することで,スタック領域やヒープ領域は常にランダム化されるが,コードセグメントに関してはコンパイル時に時にgccオプションで-fpicをつけることが必要となっている.

これらの条件を満たした状態でプログラムの実行を行うと,実行のたびにロードされる仮想アドレス空間が変動する,というのがASLRである.

KASLR

上述のASLRに対して,KASLRは名前の通り,カーネルの実行時の仮想アドレス空間を,起動のたびにランダム化するためのものである. こちらに関しても,先ほどの記事で概要が述べられている.

カーネルは,vmlinuxからbzImageになる際,stripされシンボル情報がバイナリから削除されるため,外から仮想アドレスを知ることはできない. そこで,カーネルモジュールなどからカーネルに実装されている関数を参照する際は,System.map(/boot/System.map-$(uname -r))を参照し,その仮想アドレスを知る.

System.mapには,それぞれのシンボルに対する仮想アドレスが記述されているが,KASLRにおいてはこれはただの目安となっている.つまり,実際の仮想アドレスは起動時にランダム化されていて,/proc/kallsymsの値とSystem.mapの値は異なる.

先ほどの記事から例を示す.

sudo cat /boot/System.map-4.19.0-6-amd64 | grep "T printk"
# ffffffff810e085e T printk
sudo cat /proc/kallsyms | grep "T printk"
# ffffffffad0e085e T printk

このように,異なるのがわかる.

この機能を無効にするには,nokaslrカーネルコマンドラインに追加する. 具体的には,/etc/default/grubのうち,GRUB_CMDLINE_LINUX=""となっている行を,GRUB_CMDLINE_LINUX="nokaslr"とする.

実装

KASLRの実装は,x86アーキテクチャにおいては,arch/x86/mm/kaslr.cに実装されている.(執筆時点でv5.4.3)

Randomization is done on PGD & P4D/PUD page table levels to increase possible addresses.

とあるように,ページテーブルレベルでランダム化を行う.Linuxのページング機構に関しては,以下の記事に書いた.

blog.ishikawa.tech

このファイルの主要な関数は,kernel_randomize_memory()であるが,これは/arch/x86/kernel/setup.cの中のsetup_arch()で呼ばれている.

  1. 5段階ページングが有効かどうかを確認
  2. メモリレイアウトがvaddr_start / vaddr_end変数と一致しているかどうかを確認

などの前処理を経てから最終的にここでランダム化をする.

この処理では,kernel_memory_regionに対して,

static __initdata struct kaslr_memory_region {
    unsigned long *base;
    unsigned long size_tb;
} kaslr_regions[] = {
    { &page_offset_base, 0 },
    { &vmalloc_base, 0 },
    { &vmemmap_base, 0 },
};

下にあるようなコードで

/*
* Select a random virtual address using the extra entropy
* available.
*/
entropy = remain_entropy / (ARRAY_SIZE(kaslr_regions) - i);
prandom_bytes_state(&rand_state, &rand, sizeof(rand));
entropy = (rand % (entropy + 1)) & PUD_MASK;
vaddr += entropy;
*kaslr_regions[i].base = vaddr;

page_offset_base, vmalloc_base, vmemmap_baseの値を上書きしている.

実際に,この関数のentropy加算後の値をKASLRの有効/無効で試してみたところ,

無効なマシンでは,page_offset_baseの値は,

page_offset_base: ffff888000000000

ソースで定義されているPAGE_OFFSETの値に等しいことが確認できるが,

有効なマシンでは,ffff9ee680000000となったり,再起動後は,ffff9d1100000000と変化していることが確認できた.

page_offset_base: ffff9ee680000000
page_offset_base: ffff9d1100000000

参考文献

Debian buster でデスクトップ環境を切り替える

自分のためのメモです.

環境

Debian buster 10.1

install

KDE

sudo apt install task-kde-desktop

GNOME

sudo apt install task-gnome-desktop

切り替え

環境の切り替え

sudo update-alternatives --config x-session-manager

ログイン画面の変更

sudo dpkg-reconfigure gdm3
sudo dpkg-reconfigure kde

参考

"No rule to make target 'debian/certs/debian-uefi-certs.pem', needed by 'certs/x509_certificate_list'. Stop."を解決する

Debianカーネルをビルドする際に以下のエラーが発生して,ビルドが停止した.

make
#   SYSTBL  arch/x86/include/generated/asm/syscalls_32.h
#   SYSHDR  arch/x86/include/generated/asm/unistd_32_ia32.h
#   SYSHDR  arch/x86/include/generated/asm/unistd_64_x32.h
#   SYSTBL  arch/x86/include/generated/asm/syscalls_64.h
#   HYPERCALLS arch/x86/include/generated/asm/xen-hypercalls.h
#   SYSHDR  arch/x86/include/generated/uapi/asm/unistd_32.h
#   SYSHDR  arch/x86/include/generated/uapi/asm/unistd_64.h
#   SYSHDR  arch/x86/include/generated/uapi/asm/unistd_x32.h
# .
# .
# .
# .
#   CC      kernel/memremap.o
#   CC      kernel/rseq.o
#   AR      kernel/built-in.a
#   CC      certs/system_keyring.o
# make[1]: *** No rule to make target 'debian/certs/debian-uefi-certs.pem', needed by 'certs/x509_certificate_list'.  Stop.
# make: *** [Makefile:1071: certs] Error 2
make[1]: *** No rule to make target 'debian/certs/debian-uefi-certs.pem', needed by 'certs/x509_certificate_list'.  Stop.
make: *** [Makefile:1071: certs] Error 2

解決方法を探っていたところ,この公式ドキュメントを見つけた.

Alternatively, you can use the configuration from a Debian-built kernel that you already have installed by copying the /boot/config-* file to .config and then running make oldconfig to only answer new questions. If you do this, ensure that you modify the configuration to set: CONFIG_SYSTEM_TRUSTED_KEYS = ""

とあるため,これに従い,一応リセットしたのち,.configの中身を修正する.

make clean && make mrproper
make menuconfig # menuconfigに限らず,任意の方法で.configを生成
cat .config | grep "CONFIG_SYSTEM_TRUSTED_KEYS"
# CONFIG_SYSTEM_TRUSTED_KEYS="debian/certs/debian-uefi-certs.pem"

となっているところを,

cat .config | grep "CONFIG_SYSTEM_TRUSTED_KEYS"
# CONFIG_SYSTEM_TRUSTED_KEYS=""

このように変更する.

参考文献

debファイルを用いてDebianのLinuxカーネルを簡単にアップデートする

概要

Debianカーネルをアップデートする際に,debファイルを用いることで,aptを用いてインストールをシンプルに行うことができる. また,カーネルのパッケージをdebファイルとして保持できるため,ロールバックも容易になる.

環境

Debian 9 stretch

make-kpkg

以前のDebianでは,kernel-packageに含まれるmake-kpkgというコマンドを用いてカーネルをビルドすることが一般的であった.自分はその時代のことは知らないが,少なくともDebian wheezyまではmake-kpkgを用いる方法が一般的だった.

kernel-package の古き良き時代 Linux ビルドシステムに適切な Debian パッケージをビルドする能力がなかった時代、Debian パッケージをビルドするのに推奨されていた方法は kernel-package パッケージに含まれる make-kpkg を使うやり方でした。

しかし,現在のDebianではkernel-packageは廃止されているため,別の方法を用いる.

手順

wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.2.tar.xz # 任意の場所で任意のバージョンを取得
tar -Jxvf linux-5.2.tar.xz
cd linux-5.2.tar.xz
  • ビルドに必要なパッケージをインストールする.
sudo apt-get install build-essential libncurses5-dev gcc libssl-dev bc
cp /boot/config-4.9.0-9-amd64 .config # .configという名前で保存する.
  • 変更を加えたい箇所があれば,変更する
make menuconfig

この設定画面では,vimのように,/を用いた検索をすることができる.設定項目は膨大なため,自分が行いたい設定がどこにあるか通常は見つけにくいが,検索を用いることで簡単に見つけることができる.

  • 実際にビルドをする
make deb-pkg

これでビルドが完了すると,../にファイルができる.

最後に,これらをインストールし,再起動する.

sudo dpkg -i ../linux-*.deb
sudo reboot

これで,Linuxカーネルのアップデートが完了する.

uname -a

参考

Linux KernelをビルドしてQEMUで動かす.

環境

cat /etc/debian_version
# 9.9

qemu-system-x86_64 --version
# QEMU emulator version 2.8.1(Debian 1:2.8+dfsg-6+deb9u7)
# Copyright (c) 2003-2016 Fabrice Bellard and the QEMU Project developers

必要なパッケージを取得

参考: 初心者向けKernelのビルド手順 | ガジェット好きの日記

sudo apt-get -y update
sudo apt-get -y install build-essential libncurses-dev fakeroot linux-source libssl-dev bison flex libelf-dev libelf-devel elfutils-libelf-devel

Linux Kernelのビルド

今回のビルドでは,~/build_kernelというディレクトリを作成し,その中で作業を行う.

The Linux Kernel Archivesから,好きなバージョンのLinux Kernelを落としてきて,解凍.

次に,ビルドターゲットとなるディレクトリを作成する.今回は,管理をしやすくするため,linux-4.9.183.tar.xzと同じ階層で行う.

cd ~/build_kernel
wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.183.tar.xz
tar -Jxvf linux-4.9.183.tar.xz
mkdir build # ビルドターゲット
cd linux-4.9.183

次に,Kernelの設定をする.というのもLinux Kernelでは,あらゆるCPU,あらゆるドライバに対応したソースが含まれているが, 全てをビルドすると時間もかかってしまい,出来上がるバイナリも巨大になってしまうので,ビルドしたいものとしたくないものを決めることができる.

make menuconfigで設定画面が開くので,ポチポチと設定をしていく.ちなみに僕はよくわからなかったのでほぼ初期値のまま適当に終わらせてしまった. 今後,慣れてきたら最適化をしていこうと思う.今回はとりあえずビルドして,QEMUで動くが動かないかを試すだけなので一旦これでいい.一旦.

設定が終わったら,makeする.この際に,ビルドのターゲットをさっき作ったディレクトリに設定するのを忘れないようにする.

make menuconfig
make O=../build/kernel

とても時間がかかるので待つ.

BusyBox

この章はほぼ下の参考にしたサイトと同じことをやっているので,そっちを見た方が早い.が一応自分でも書く.

参考: ビルドしたLinuxカーネルをブートできる最低限の環境を用意する(with Busybox & qemu) - 豆腐の豆腐和え

Linux Kernelにはユーザーランドの実装が含まれていないため,最低限のコマンドすらない. BusyBoxとは,この最低限のコマンドを用意してくれているやつ.(?)

とりあえず準備してみる.BusyBoxからソースを落とす.

解凍したら,make menuconfigで設定する.ここでは,ライブラリを静的リンクするように設定する.(staticとあるところを探す.) 完了したら,ビルド.

ビルドが完了すると,busybox-1.31.0/_installというフォルダができているので移動してコマンドを叩く.

起動後ルートファイルシステムをになるファイルを作る

らしい.

cd ~/build_kernel
wget https://busybox.net/downloads/busybox-1.31.0.tar.bz2
tar -jxvf busybox-1.31.0.tar.bz2
cd busybox-1.31.0
make menuconfig # ライブラリを静的にリンクするように設定
make install
cd _install
find . | cpio -o --format=newc > ../rootfs.img

QEMUで起動する

ビルドが完了したので,QEMUで起動する.

~/kernel_build/build/kernel/arch/x86/boot/bzImageというものができているので,これを使ってビルドする.

qemu-system-x86_64 -kernel ~/kernel_build/build/kernel/arch/x86/boot/bzImage -initrd ~/kernel_build/busybox-1.31.0/rootfs.img -append "root=/dev/ram rdinit=/bin/sh"

以上で,特になんの変更も加えてないLinux Kernel + BusyBoxを動かすことができた.

今後は,各Kconfigを適切に設定し,ビルド時間の短縮など,より最適化していく.

参考

Linux(2.6.11)において,int 0x80命令によってシステムコールが呼ばれる流れを追ってみる

Linux(2.6.11)において,int 0x80命令によってシステムコールが呼ばれる流れを追ってみた

この記事では,以下の本を参考に,実際にコードを見ていく記事.

参考文献: Linuxカーネル2.6解読室

Linuxカーネル2.6解読室

Linuxカーネル2.6解読室

そもそもシステムコールとは

ウィキペディアにはこのように書いてある.

オペレーティングシステム (OS)(より明確に言えばOSのカーネル)の機能を呼び出すために使用される機構のこと。 実際のプログラミングにおいては、OSの機能は関数 (API) 呼び出しによって実現されるので、OSの備える関数 (API) のことを指すこともある。

例えば,プログラムから見て,外界に繋がる操作をする際は,システムコールを使用する. システムコールでは,デバイスを扱う処理などをOSが引き受けることで,より安全に,確実に処理を行えるようになる利点がある.

Linux 2.6におけるシステムコールの機構

i386 / x86_64 アーキテクチャでは,システムコールを実行する方法として,4つの方法を提供している.そのうち,Linuxで使用しているのは,以下のチェックが入った二つの方法を使用している.

  • [x] ソフトウェア割り込み(int n命令)
  • [ ] コールゲート
  • [ ] タスクゲート
  • [x] sysenter命令を使用

システムコールは,基本的にはglibcライブラリが,定められた手順によって実際に発行されるため,プログラマはそのAPIがライブラリルーチンなのか,システムコールなのかを知る必要はない. 例えばopenシステムコールでは,一旦は,glibcのライブラリを呼び出し,その中の処理でシステムコールを発行している.

システムコールはそれぞれに番号が振られているが,カーネル内では,この番号に応じた関数が定義されている.内部的にシステムコールの実態のなる関数は配列に定義されており, 前述のシステムコールの番号というのは,この配列のインデックスとなっている.

Linuxカーネル2.6/i386アーキテクチャでは,約270のシステムコールが定義されている.

/include/asm-i386/unistd.h

#ifndef _ASM_I386_UNISTD_H_
#define _ASM_I386_UNISTD_H_

/*
 * This file contains the system call numbers.
 */

#define __NR_restart_syscall      0
#define __NR_exit        1
#define __NR_fork        2
#define __NR_read        3
#define __NR_write       4
#define __NR_open        5
#define __NR_close       6
#define __NR_waitpid         7
// 省略....
#define __NR_sys_kexec_load    283
#define __NR_waitid        284
/* #define __NR_sys_setaltroot 285 */
#define __NR_add_key       286
#define __NR_request_key   287
#define __NR_keyctl        288

ちなみに,Linux2.6時点でのx86_64のシステムコールここにある.

C言語においては,システムコールの呼び出しとの変換は,glibcが行なっており,呼び出し方法としては,int 0x80/iret命令か,sysente/sysexit命令が用いられる.

システムコールを呼び出す際は,特定のレジスタにそれぞれ対応した値を格納したのちに,int 0x80命令なりsysenter命令を実行することで,処理が行われる.

int 0x80を使ったシステムコール

32bit(i386)の場合

  • eaxに呼び出すシステムコールの番号
  • ebxに0番目の引数
  • ecxに1番目の引数
  • 以降,edx,esi,edi,ebpと続いていく.

64bit(x86_64)の場合

  • EAX:呼び出すシステムコールの番号
  • RDI:0番目の引数
  • RSI:1番目の引数
  • 以降,RDX,R10,R8,R9と続いていく

例えば,x86_64 Linuxwriteシステムコールを使いたい場合は,

mov    eax,0x1 # 1番のシステムコールを呼びたい.
mov    edi,0x1 # 0番目の引数
movabs rsi,0x601001 # 1番目の引数
mov    edx,0x1 # 2番目の引数
syscall # まだ出てきていないが,x86_64においてシステムコールを呼び出すもの.

のようにすると呼び出すことができる.

では実際に処理を見ていく.

通常のC言語を用いたプログラミング(インラインアセンブラなどを用いない)では,先ほども書いたように,glibcが実際のシステムコールの呼び出しを行う. ライブラリが,レジスタに引数を設定したのち,int 0x80命令を実行する.すると,CPUは特権モードに移り,system_callから実行が始まる.

https://elixir.bootlin.com/linux/v2.6.11/source/arch/i386/kernel/entry.S#L241

ENTRY(system_call)                                                  # 1
    pushl %eax            # save orig_eax                             # 2
    SAVE_ALL                                                        # 3
    GET_THREAD_INFO(%ebp)                                           # 4
                    # system call tracing in operation
    testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
    jnz syscall_trace_entry
    cmpl $(nr_syscalls), %eax
    jae syscall_badsys                                              # 6
syscall_call:
    call *sys_call_table(,%eax,4)                                   # 7
    movl %eax,EAX(%esp)     # store the return value
syscall_exit:
    cli              # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    movl TI_flags(%ebp), %ecx
    testw $_TIF_ALLWORK_MASK, %cx  # current->work
    jne syscall_exit_work
restore_all:
    RESTORE_ALL

    # perform work that needs to be done immediately before resumption
    ALIGN

https://elixir.bootlin.com/linux/v2.6.11/source/arch/i386/kernel/entry.S#L84

#define SAVE_ALL \
    cld; \
    pushl %es; \
    pushl %ds; \
    pushl %eax; \
    pushl %ebp; \
    pushl %edi; \
    pushl %esi; \
    pushl %edx; \
    pushl %ecx; \
    pushl %ebx; \
    movl $(__USER_DS), %edx; \
    movl %edx, %ds; \
    movl %edx, %es;
    cmpl $(nr_syscalls), %eax
    jae syscall_badsys
  • 7: システムコール関数を実際に呼び出しているところ.
    • ここで先ほど指定されていた番号のインデックスを辿り,関数を実行している.

https://elixir.bootlin.com/linux/v2.6.11/source/arch/i386/kernel/entry.S#L575

ENTRY(sys_call_table)
    .long sys_restart_syscall  /* 0 - old "setup()" system call, used for restarting */
    .long sys_exit
    .long sys_fork
    .long sys_read
    .long sys_write
    .long sys_open     /* 5 */
# 省略
    .long sys_ni_syscall       /* reserved for kexec */
    .long sys_waitid
    .long sys_ni_syscall       /* 285 */ /* available */
    .long sys_add_key
    .long sys_request_key
    .long sys_keyctl

上で例にあげたwriteシステムコールの実装を探してみると.

https://elixir.bootlin.com/linux/v2.6.11/source/arch/i386/kernel/entry.S#L580

https://elixir.bootlin.com/linux/v2.6.11/source/fs/read_write.c#L330

システムコールは,カーネルの中に定義された関数であるということがわかった.

これ以降の処理は,ユーザーモードに戻る準備をしている.

sysenterを使ったシステムコール

sysenter, 32bit(i386)の場合

  • eaxに呼び出すシステムコールの番号
  • ebxに0番目の引数
  • ecxに1番目の引数
  • 以降,edx,esi,ediと続いていく.
  • ebp,5番目の引数へのポインタで,ユーザープロセスのスタックを指す.

最後だけ少し違う.

今回は省略.

次回以降

この記事では,i386(32bit)Linux 2.6.11のコードをみるに止まった.今後は,最新版Linuxx86_64の処理を追ってみたいと思う.

参考

セグメント機構とCPUの動作モード(Linux(64bit)との関係性)

この記事はsfc-rg Advent Calendar 2018 5日目の記事です.

遅くなってしまいました.ごめんなさい.

概要

セグメント機構とそれに付随するCPUの動作モードに関する記事.

あとで自分が見るときのために,CPUのセグメント機構について大雑把に説明し,32bit版と64bit版の違いなどについて触れていきたい.

また,セグメント機構をLinuxが32bitと64bitの場合で,それぞれどのように使われているかをまとめていく.

この記事で扱うLinuxは,x86およびx86-64アーキテクチャにおけるLinuxとする.

セグメント機構とページング機構

前回の記事からの引用,

blog.ishikawa.tech

x86アーキテクチャおよびx86-64アーキテクチャには,その上に乗るOSのアドレス変換機構をサポートする機能がある. それが,セグメント機構と,ページング機構である.

論理アドレス --[セグメント機構]--> リニアアドレス --[ページング機構]--> 物理アドレス

それぞれの役割は上の通り.つまり,CPUには二段階の変換機構がハードウェアレベルで用意されているということ. セグメント機構は,無効にすることはできないが,ページング回路は,有効無効を切り替えることができる.OS次第. 上に出てきている,リニアアドレスというのが,いわゆる仮想アドレスのことであり,Linuxにおいては,これがプロセス空間にて使用される.

セグメント機構には,CPUの動作モードに依存した,何種類かある.まずは,前提となるCPUの動作モードからまとめていく.

CPUの動作モード

CPUはブート時,8086互換環境を提供するリアルモードで動き出す.

その後,32bitのプロテクトモードに移行し,最後に64bitモードに移行する.

それぞれのモードの説明

リアルモード

8086用にかかれたプログラムを実行させるためのもの. 電源投入直後はやリセット後は,一時的に,リアルモードで動作する.

リアルモードが扱えるメモリ空間は,20bitで表現できる,0x00000 ~ 0xFFFFFまでの1MB. 20bitなのは,8086のアドレスバスが20本しかないため.

最初の処理が終わったら,プロテクトモードに移行する. アクセスを保護する機能などはない.

プロテクトモード

プロテクトモードは,80386以降に搭載された,メモリ管理やタスク管理などを使うためのモード.

プロテクトモードが扱えるメモリ空間は,4GBまで広がった.

リアルモードはプロテクトモードとの互換は一切ない.

メモリやI/Oデバイスへのアクセスを保護する機能が常に働いている.許可されない範囲のアドレスを,プロセスが利用としたら,CPUは割り込みを発生させる.

仮想8086モード

プロテクトモードのメモリ管理などの機能を働かせたまま,8086用プログラムが実行できるようになるモード.このモードはLinuxでは使われていない.

64bitモード

プロテクトモードのアドレスを64bitに拡張したモード.理論的には,64bitで表せるアドレスの範囲を扱えるが,現在は,40bitしか扱えないようになっている.

互換モード

64bitモードの中で,32bit用のアプリケーションを動かすためのもの.

リアルモードからプロテクトモードへの移行

CPUが動作モードは,CR0と呼ばれるレジスタの最下位bitの内容に基づいて決まる.

この値が1であれば,プロテクトモード,0であれば,リアルモード.

動作的には簡単だが,これだけは移行はできない.

  • GDT(Global Descriptor Table)の作成
  • GDTレジスタの設定
  • IDT(Interrupt Descriptor Table)の作成
  • IDTレジスタの設定
  • A20のマスク解除
  • CPUへの割り込み禁止
  • CR0レジスタの再開ビットを1に
  • パイプラインの内容をフラッシュ
  • セグメントレジスタの設定

これらを設定する必要がある.

レジスタ一覧

セグメント機構の概要

セグメント方式とは,連続したメモリ空間を区画に分割して管理する方式の名前.その区画のことをセグメントと呼ぶ.

セグメントの中の特定のアドレスは,オフセットで表す.

IA-32(32bit)のCPUにおいては,0番地から,高位に向かって連続するアドレス空間の,ある大きさをセグメントとして認識する. この時に指定すべき値としては,セグメントベースと呼ばれる開始アドレスと,大きさである.

セグメント内のアドレスは,セグメントベースにアフセットを加えて指定し,このアドレスをリニアアドレス(仮想アドレス)と呼ぶ.

セグメントベース + オフセット = リニアアドレス(仮想アドレス)

ここでこのセグメントを指定する方法の解説をしていく,

セグメントレジスタの値から,セグメントベースを算出し,この値をオフセットを足し合わせることで,リニアアドレスを算出している.この,算出方法については,CPUのモード毎に異なる.

通常,システムには複数のセグメントが存在し,その領域同士が重なっている場合があるが,これは適切に設定できていれば問題ない.

上の図にも書いてあるが,この,セグメント機構にかけられる前のアドレスを,論理アドレスと呼ぶ.論理アドレスのい書式は以下の通り.

レジスタ名:オフセット値

レジスタ名:汎用レジスタ名

例えば,DS:0100は,DSレジスタが指すセグメントの,先頭から数えて,0x0100番目のアドレスを指す.

また,直接レジスタの値を使うこともある.

レジスタ値:オフセット値(例: 2345:0100)

セグメントレジスタの用途

6つあるセグメントレジスタのうち,それらのうち,CS, DS, SSレジスタは,用途が決められている.

CSレジスタ

Code Segment

CPUが実行するプログラム,つまりプロセスにおけるコードセグメントに相当する領域を指定するために使われる.

IPレジスタ(インデックスポインタ.32bitではeip,64bitでは,ripと呼ばれる)は,実行中の命令のアドレスのオフセット値を保持し,解釈している.

DSレジスタ

Data Segmentを指定するためのレジスタ

SSレジスタ

Stack Segmentを指定するためのレジスタ.スタックの先頭アドレス.

リアルモードにおけるセグメント

8086では,セグメントレジスタの値は,16bitしかない.しかし,これを4bit左シフトすることで,20bitのセグメントベースとしている.ここに,オフセットを加算し,仮想アドレスを算出する.

オフセットの大きさは16bitであり,一つのセグメントの大きさは,常に64KBになる.

あるセグメントレジスタの値が0の時,作り出せる仮想アドレスの範囲は,

0000:0000 ~ 0000:FFFFの16bitの範囲となり,仮想アドレスの範囲は,0x00000 ~ 0x0FFFFとなる.

表記上,異なる論理アドレスに見えても,計算した結果,同じ仮想アドレスが算出された場合,その二つの論理アドレスは,同じものであると考えることができる.

プロテクトモードにおけるセグメント

プロテクトモードのCPUでは,32本あるアドレスバスを全て使えるようになるため,オフセットアドレスに32bitの値が使えるようになった.

そのため,プロテクトモードにおいては,4GBあるメモリ空間の全ての仮想アドレスを直接指定できるようになった,

セグメントレジスタの値の大きさは,プロテクトモードにおいても,16bitである.

リアルモードにおけるセグメントレジスタの値は,4bit左にシフトされていたが,プロテクトモードにおいては違う.

プロテクトモードのCPUはセグメントディスクリプタと呼ばれる特別なデータを必要とする.これは,8バイトの構造体である.

これはメモリ上に作成されるデータであり,この構造体を並べて作成し,その開始アドレスと個数をCPUに通知する.

この.並べて作られたセグメントディスクリプタの塊をセグメントディスクリプタテーブルと呼ぶ. プロテクトモードのセグメントは,一つのセグメントに対して,一つのディスクリプタを用意する.

セグメントレジスタは,セグメントディスクリプタテーブルの中から一つのテーブルを指すための,インデックス値を格納する. セグメントセレクタの値は,セグメントレジスタの15bit - 3bitに入っている,これを8倍すると,テーブルのバイトのオフセットが求められる.(デスクリプタの値が8バイトであるため)

セグメントセレクタの値が全て0の場合,セグメントレジスタを無効にする,という意味がある.

こうすると,セグメントディスクリプタテーブルの0番目のエントリが使えないことになるので,0番目のエントリには空のエントリが作成される.

セグメントディスクリプタの構造

セグメントディスクリプタのサイズは,8バイト,つまり64bitである.

それぞれのフィールドの説明をしていく.

ベース 31bit - 0bit

ベースと呼ばれるフィールド.セグメントのベースアドレスが格納されている.

リミット 19bit - 0bit

セグメントのサイズを表す. サイズの単位は,後述のGフラグの内容に依存する

Gフラグが

0の時,1MBまでの値

1の時,4GBまでの値

Gフラグ 55bit

リミットの単位.

Dフラグ 54bit

リアルモードでは,アドレスのサイズ,データのサイズは16bitであった.一方,プロテクトモードでは32bitを選択することもできる.

Dフラグが1の時,32bitとなり,0の時,16bitとなる.

Linuxについては,常に1が設定される.

Pフラグ 47bit

セグメントがメモリ上に存在するときは,1.しないときは0となる.

Linuxでは,セグメント全体がスワップアウトされることがないため,常に1となる.

DPL 46-45bit

Descriptor Privilege Level

デスクリプタ特権レベルと呼ばれ,そのセグメントへアクセスするのに必要なCPUの特権レベルを表す.

タイプ 43bit - 41bit

セグメントのタイプを表す.

基本的に,chmodとかで使う,あの数字っぽい感じ.

プロテクトモードにおけるセグメント続き

以上の形式で,セグメントディスクリプタテーブルがメモリ上に作成されると,その先頭アドレスとエントリ数がCPUに通知される.

この命令を実行すると,GDTRレジスタに32bitの先頭アドレスと,16bitのエントリ数が書き込まれる.

しかし,ここまでやって言うのもあれだが,Linuxにおいては,セグメントの機能はあまり出てこない.

Linuxが使用しているセグメントは以下の4つ.この4つは,セグメントの特権レベルやタイプが違うだけもの.

  • KERNEL_CS
  • KERNEL_DS
  • USER_CS
  • USER_DS

全てのセグメントは,ベースアドレスが0から始まり,その大きさは,4GB.つまり,オフセット = 仮想アドレス

この方式のことを,基本フラットモデルという.ほとんど機能していない,あってもなくても同じに見えるセグメント機構であるが,なぜ使われているのか.

その理由は単純で,セグメント回路を無効にすることができないからである.

OSによっては使われているが,少なくともLinuxにおいては,使われていないというのが現状である.

64bitにおけるセグメント

64bitモードでは,DS,ES,SSの3つのセグメントレジスタは使用されない.

CPUのアーキテクチャ上は,コードセグメントの指定に,CSレジスタのみ使う.

しかし,Linuxにおいては,32bitの時と同様,フラットモデルで使われるため,どうでもいいこと.

その他,微妙に,各フラグの使い方は異なるが,きにする必要がない.

結論

Linux(64bit)では,セグメントがほぼ使われていないため,勉強する必要がない.

参考文献

Linux(64bit)のページング機構

この記事はsfc-rg Advent Calendar 2018 4日目の記事です.

概要

Linuxのページング機構について勉強する必要があったが,書籍やインターネットにある情報は,32bit版のものが多く,64bit版の情報を探すのに苦労した.

今後自分のような人が出てきた際に,困らないように,32bit版と64bit版の違いなどについて書く.

この記事で扱うLinuxは,x86およびx86-64アーキテクチャにおけるLinuxとする.

セグメント機構とページング機構

x86アーキテクチャおよびx86-64アーキテクチャには,その上に乗るOSのアドレス変換機構をサポートする機能がある.

それが,セグメント機構と,ページング機構である.

論理アドレス --[セグメント機構]--> リニアアドレス --[ページング機構]--> 物理アドレス

それぞれの役割は上の通り.つまり,CPUには二段階の変換機構がハードウェアレベルで用意されているということ.

セグメント機構は,無効にすることはできないが,ページング回路は,有効無効を切り替えることができる.OS次第.

上に出てきている,リニアアドレスというのが,いわゆる仮想アドレスのことであり,Linuxにおいては,これがプロセス空間にて使用される.

セグメント機構

セグメント機構に関しては,この記事では扱わない.以下の記事で解説した.

blog.ishikawa.tech

ページング機構

上述の通り,有効無効を切り替えることができる.デフォルトではオフになっており,この場合は,仮想アドレスと物理アドレスが等価となる.

これからは,ページング回路が有効な場合について書く.

ページング回路は,アドレス空間を固定長(4KB or 4MB)のブロックに分割し.ブロック単位でアドレス変換を行い,これらのブロックのことをページフレームと言う.

アドレス変換に必要なページフレームは,メモリ上に置かれ,その物理アドレスをCR3レジスタに格納する.つまり,プロセスごとにCR3レジスタの値が存在するということ.

このCR3レジスタの値は,プロセスのアドレス空間を保持する上でとても大事な役割を果たすため,ユーザープロセスからの書き換えはできない.

また,このページフレームをうまく設定することで,同じ物理アドレスを複数のアドレスから参照することもできる.

このページング回路により,プロセスからはあたかもメモリ全体を占有しているように見える. しかし,プロセスごとに,メモリ空間全体を与えていては,メモリがいくらあっても足りなくなってしまう. なので,この仮想アドレスに対応する物理アドレスはその先に存在しない,というときに,そういうフラグをエントリのなかに立てる.こうすることで,メモリ空間をいい感じに扱うことができる.

ページングのアドレス変換は,ページフレーム単位で行われる. 仮に,一段階で全てのページフレームを扱おうとすると,32bitの場合,4GB / 4KB = 1048576個必要になる.しかし,一つのプロセスが,4GBのメモリ空間を使うことはない. そうなると,ほとんどが未使用の穴だらけのテーブルが完成してしまう.こうすると,無駄が大きい. そのため,32bitでは,2段階,64bitでは,4段階の変換テーブルが用意されている.こうすることで,プログラムの大きさが小さい場合に,2番目以降の変換テーブルを設定する必要がなくなり,無駄が削減できる.

各テーブルとエントリ

基本的な役割は32bit,64bitで同じだが,各エントリのbit数,エントリ数などが微妙に異なる.

共有

各エントリの0bitには,Pフラグが割り当てられている.このフラグが1となっているとき,そのエントリがさすページフレーム,物理アドレスは存在しないということとなる

各エントリにある値は,便宜上物理アドレスとしているが,実際の物理アドレスは,エントリに12個0を付け足したものとなる.こうすることで,4KB境界に合わせられる.64bitの場合は,28bitでベースアドレスと指定しているので,現在のCPUでは,40bitまでのアドレス空間しか使うことができない.

32bit

32bitにおいては,変換テーブルが二つ存在する.

1段階目の変換テーブルは,ページディレクトリと呼ばれる.ページディレクトリの大きさは,4KBで,4Byteのページディレクトリエントリが1024個ある.

このサイトに書いてあるように,ページディレクトリエントリのインデックスは,仮想アドレス空間の31bit - 22bitに格納されている.10bitであり,エントリ数の1024と等しい.

4Byte = 32bitのうち,31bit - 12bitにページテーブルの物理アドレスが格納されている.仮想アドレスで指定されてインデックスを見て,そのインデックスのエントリから二段階目の物理アドレスを読み,次の階層に進む.

2段階目の変換テーブルは,ページテーブルと呼ばれる.ここにもページディレクトリと同様,4Byteのエントリが1024個あり,テーブルのサイズは4KBである.先ほどと同様,仮想アドレスで指定されているインデックス(仮想アドレスの21bit - 12bit)を見て,該当エントリに飛ぶ.そのエントリの31bit - 12bitにページフレームのベースアドレスが格納されている.ここに,仮想アドレスのオフセット部分(11bit - 0bit)を足すことで,仮想アドレスが指す物理アドレスを割り出すことができる.

64bit

64bitにおける変換機構は32bitのものと少しだけエントリの数とサイズが違う.

64bitでは,ページディレクトリ(Page Directory),ページテーブル(Page Table)の上に,ページマップレベル4(Page Map Level 4)という階層と,ページディレクトリポインタ(Page Directory Pointer)という階層が追加されている.

順番でいうと,32bitが

  1. ページディレクト
  2. ページテーブル

であったが,64bitでは,

  1. ページマップレベル4
  2. ページディレクトリポインタ
  3. ページディレクト
  4. ページテーブル

となっている.

テーブルのサイズは4KBで固定であるが,64bitでは,扱うべきアドレス幅が大きくなっている. 各エントリは,32bitのときは4Byteであったが,64bitでは,8Byteとなっている.これに伴い,各エントリ数は,512個となっている.(8Byte * 512 = 4KB)

ページサイズや変換の階層が異なったりと,他にも色々なモードがあるが,とりあえず基本的なもののみ解説した.

参考文献

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

(アスキー書籍)

参考にしたサイト

Cyber AgentのエンジニアJOBに参加した

この記事はなに?

Cyber AgentのエンジニアJOBというイベントに応募し,1ヶ月インターンさせていただいた時の記録と,得られたものや,反省点などを書いたもの.

参加させていただいた部署は,FRESH LIVEという,ライブ配信サービスを行う部署.

8月は,この部署で1ヶ月フルタイムで働いた.

したかった経験

自分はスタートアップでエンジニアとして,1年半働いていた.当初はギリギリ動くプログラムがかけるくらいの超初心者だったが,とりあえず飛び込んでみたのが今から1年9ヶ月ほど前.そこから1年半強経ち,新しい環境に飛び込んでみたくなったため,今年の6月に退職し,こちらの部署にお世話になることとなった.

挑戦はいっぱいしたいが,1ヶ月という短い期間なので,ある程度目標を立てて参加した.部署を決める前に,人事の方に相談(希望)した内容が以下の点.

吸収したいこと

  • 大規模トラフィックあるいは大規模トラフィックの経験
    • 前の会社で大規模データの壁にぶつかり,解決法を模索したため
    • インフラレベルでの解決法など
  • 高品質なサービスを継続して提供できるエンジニアのレベルを肌で感じたかった
    • テストをまともに書いたことがなかったので,テストをどこまで書くのかなど.
    • 開発スピードや,知識量,アウトプットや考え方など.
  • インフラ周り
    • 調べたところ,k8sの導入などが進んでいた

希望したこと

  • バックエンド
  • 言語はなんでもいい!
  • メディア系の事業部

これらの希望を出したところ,FRESH LIVEで働くこととなった.

何をやったか

FRESH LIVEでは,マイクロサービスアーキテクチャを採用している.また,言語はKotlin(つまりサーバーサイドKotlin)を採用している.こちらのスライドに書いてある.

この一連のアーキテクチャにおいて,とあるマイクロサービスの実装を1ヶ月でやってみた.要件としては,

  • 機能追加時に柔軟に対応できる設計
  • gRPC通信
  • Kotlin(本当はGo(これなら書いたことあった)でもよかったらしいが,事業部の方針としてKotlinだったのでKotlinで頑張ってみることにした)
  • Spring Boot 2
  • Gradle

記録

最初の方

これで1ヶ月頑張るということで,インターン2日目くらいに要件が固まったが,当初は2つ目以降の要件がどうすればいいのか検討もつかない状態だった.

そもそもその単語が指し示す技術がどこでどうやって動くのかすらわからなかったので,最初の数日は,調べたりイメージをつかむことに費やした.特に,自分はJava及びJVMを使ったことがなかったため,Spring Boot 2Gradleもわけわからなかったし,Kotlinがどういうノリで動くのかもわからなかった.また,調べれば調べるほど知らない単語が出てくるので,脳みそが限界を迎えた時もあった.

gRPC通信に関しては,Javaと分離できる技術だったので,一旦Goで動かしてみて,イメージを掴んだ.

設計

2週目からは,設計を始めた.が,設計はアウトプットが出にくい故に,今やっていることが正しいのかわからなくなる時があり,気を紛らわせるために,Gradleを調べつつ書いていったり,Kotlin書いてみたりしてた.

機能追加時に柔軟に対応できる設計ということで,イメージを鮮明にする意味でも,一旦Kotlin書き始めたのは,間違っていなかったと思う.

リクエストのインタフェースの設計

リクエストのインタフェースの設計をする上で,gRPCというRESTとはまた違ったものになるので,慣れるのがとても大変だった.ここら辺は,ドキュメントを読んだり,他のリポジトリを参考にしたり,ツッコミをもらいながら,だんだん確立させていったが,最後の方までなかなか最適な答えにたどり着けず,設計の難しさを感じた.

OOPの設計

オブジェクト指向言語をちゃんと触ったのが初めてだったため,ここの設計もかなり苦労した.(概念は調べたりすることでわかってはいたり,すでにあるクラスを使うくらいはできたが,設計を1からした経験がなかったという意味の初めて)

設計やクラスについてググってみると,なかなか自分の知りたい情報にたどり着けず,こういう領域はまたちゃんと勉強しないとできるようにはならないんだなと感じた(なんとなくセンス()だけでは全然ダメ).

また,指摘を受けたのが,クラス名などの名前がめちゃくちゃで,何をやっているクラスなのか名前からわからないという点.これはあんまり意識できていなかったというか,言われて,「あ,たしかに」と思った.こうなってしまった理由として,クラスに対して,そのクラスが何をして,何を知っていて,何をするクラスなのかというのをあまり意識せず,今までの関数に切り出すぞくらいの気持ちでやっていたのがよくなかった.

DIという概念

これは実装を進めていく過程で指摘を受けた事項.上述の通り,自分は恥ずかしながら,今までテストをまともに書いたことがなかった.今回はそのコンプレックスを解消すべく,テストを書くことにしていたのだが,そこでDIという概念を教えていただいた.

DIというのは,現在の自分の理解では,

クラス内でインスタンス化すると,クラスの単体テストを実行する時に,モック化できていないと,結局そのあとに続くクラスを全て実装するまでテストができない.

そのため,クラスのインスタンス生成をDIコンテナに任せ,そこからクラスの引数として,DIコンテナからオブジェクトを受け取る.

Springにはそれを簡単にしてくれる仕組みがあるので,それを使うと,DI化できる!テストを書く時,オブジェクトをモック化できる!

という感じ.

デザインパターン領域は今まで全く意識を向けなかった領域だったのだが,DIの話を聞いて,なるほどと思った.

おそらくこういうのは,適当に作った結果苦しんだ人が,後世の人が苦しまないように知恵を絞った結果だと思うので,一回適当に書いて苦しむと,より恩恵を受けられるのかなと感じた.

実装

実装自体は,設計が決まれば進めていくだけだった,

機能的には膨大な要件ではなかったので,コード量的にも多くなかった.

Kotlinの文法などに関しては,途中から結構慣れてきた(詳しい部分はわからない,ビルドでエラーを吐く頻度が減っていったという意味)ので,あまり問題にはならなかった.

設計の適当さによって頭を整理できずにコードを書くことがあった.これをメンターの方に相談したところ,「設計の方が頭も時間も使う」と言われ,それを身を以て実感した感じとなった.

結果

求められていた2個の機能のうち,1個しか実装を開始することができなかった.

その,できた1個も,機能として完璧になったとは言えない結果になった(単純な実装になってしまった).

しかし,Kotlin的なコードの書き方やテストなど,「それっぽいコードにはなってきた」とは言っていただけた.

感想(主に今後の課題)

アウトプットの質

基本的にアウトプットを文章で伝えていたのだが,これがなかなかうまくできなかった.自分の頭にあるイメージを断片的にしか伝えられず,メンターの方に思考を整理してもらってやっと共有できる状態になった.

設計が大事

設計がとても大事.とりあえず書き始めるというスタイルは後々の苦労を招くことがわかった.

実装前に,全ての処理を頭の中に思い浮かべて「あとは書くだけ」という状態にしなけばならない.

今後はもっと詳細に設計をイメージして,アウトプットを書いていこうと思った.

一ヶ月をやり直せるなら、設計段階でもっと先を見通せと自分に言いたい。

JVMわけわからん

今までJVM言語を避けて生きてきた.今後もそのままだと,エンジニアとしてちょっとまずい気がしていたのだが,巨大なすぎてなかなか一歩を踏み出せなかった.しかしその壁を,このインターンで越えることができたので本当によかった.

ただ,どこで落ちているのかなどが文法エラー以外でわからないことが多かった.今後も勉強して頑張っていけたらと思う.

まとめ

1ヶ月,自分の中ではかなり頑張った方だと思うが,振り返ってみると,成果物があまりない.もっと実装を早くできていたら,それらのデプロイに伴って,インフラを触ることができたのに,と思うと自分の実力不足がとても悔しい.

当初の目標でいうと,インフラを触ることはできなかったが,これについては,メンターの方にお話を伺うことができた.当然ここに詳細は書けないが,インフラの自動化ってめっちゃ楽しそうだなと思った.

当初の目標だった高品質なサービスを継続して提供できるエンジニアのレベルを肌で感じたかったに関してはなんとなくわかった気がする.これはうまく言葉では言い表せないが,体感できた.

Slackに自分の分報があって,そこに疑問などを投げたらすぐに返事をいただけたり,コードレビューを"ど素人みたいな"コードにしていただけたり,細かくフィードバックをいただけたりと,知らないことだらけの技術の中で,とても安心感があった.また,ランチにも頻繁に連れていっていただき,色々な方と話すことができたのもとてもよかった.

1ヶ月という短い期間ではあったが,とっても学びや反省の多い1ヶ月だった.

最後になりますが,サポートしていただいたみなさま,ありがとうございました!苦しみつつも,本当にいい時間を過ごせました.

プロセスの概要 UNIX V6の実装を読んだ

研究室にて,UNIX V6のソースコードを読んでみる本を輪講した.

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

この輪講を通して,OS(オペレーティングシステム)のイメージ(どこでどのように動いているのか等)が以前に比べてより鮮明になったかなと感じている.

今回の記事では,OSの機能のうち,プロセスに関することをカーネルソースコードを意識しつつ書いていこうと思う.

なぜUNIX V6なのか

OSに関する本を読んでみたいという気持ちがあった.でもLinuxだと難しいし,本も分厚い,挫折しそうと,と危惧していたところ,ファカルティの方から,U NIX V6ならソースコード短い(全部で1万行)し,やってみれば,との助言をいただいたので,やることにした.

書籍の紹介にも,

UNIX V6は1975年にベル研究所からリリースされたOSですが、近年のOSにも通じる、OSの基本的なアイディアが詰まっており、 デバイスドライバも含めて約10,000行という、カーネルの全体を理解するのが難しくないボリュームに収まっています。

とあるので,やることにした.

全ての要素をここで詳細に書くと,それはもう本を買って読めばいいじゃんとなるので,なるべく抽象的に書けるように気をつける.

ちなみに,Linuxカーネルソースコードは144MBあるらしい.これは,オライリーの大型本にして,40000ページ分.「詳解Linuxカーネル」40冊分.

OSの主な機能

まずはOSが何をやっているか?であるが,大まかに分けると以下のようになる.(本の目次まま)

プロセス

プログラムの実行に関して

前提として,プログラムの実行に関して記述する.

通常のプログラムは,プログラマがソースを書いて, (それがコンパイル型言語であろうと,インタプリタ型言語であろうと,)どうにかして各プロセッサの命令に落とし込む.

例えばC言語の場合,ソースコードgccなどでコンパイルして実行ファイルを作り,実行する.

実行すると,実行ファイルはメモリ上に展開され,プロセスとして実行される.

実行中のプロセスの仮想アドレス空間をみてみると,その中にはテキストセグメントとデータセグメントが存在し,テキストセグメントの命令に沿ってプログラムが進んでいく.

テキストセグメントは通常,プロセスの仮想メモリ空間上の,最下位(0x0)に位置し,ReadOnlyである.

データセグメントにはデータ領域とスタック領域があり,データ領域はテキストセグメントのすぐ後のある位置からスタートし,最上位アドレスに向けて伸びていく.

スタック領域は,仮想アドレス空間上の最上位からスタートし,最下位アドレスに向けて伸びていく.

今回のアーキテクチャでは,データセグメントの下位アドレスに,PPDA(Per Process Data Area)というカーネルにしか見えない領域が物理的に確保されている.

カーネルの実体

カーネルも,捉え方によっては一つの巨大なプログラム,プロセスであると捉えることができる.

物理アドレスの最下位アドレスに,カーネルのテキストセグメントは存在からである.

この巨大なプログラムが,直接CPUなどのハvードウェアそ制御したり,プロセスの管理をしたりしている.

カーネル内部には,プロセスを開始する関数が書かれており,ユーザーがプログラムを実行しようとすると,この関数が呼ばれる.

プロセスのリストは,カーネル内のグローバル変数,proc[50] (procというのは,プロセスの情報が入っている構造体)として管理されており, 仮想アドレス空間を構築したりした後に,この配列に書き込むことで,プロセスの一覧を保持している.(こう書くと,カーネルも実行中のプログラムなんだなと考えることができる)

仮想アドレス空間

実行中のプログラムは,以上のようにの仮想アドレス空間にて実行されている.これを実現しているのは,MMU(`Memory Management Unit)という機構で,これについては後述する.

仮想アドレス空間という方式を採用することで,

  • 各プログラムが実際にどの物理アドレスに書き込んだり読み込んだりしているか意識する必要がない
  • 各プログラムが別のプログラムのメモリ領域をみることができない

というメリットがある.

カーネルは,プログラムを実行する際に,仮想アドレス空間を構築する,これはつまりMMUの値を設定し,proc構造体か,その子分みたいな存在のuser構造体にこの値を保存する.

この,「仮想アドレス空間を構築」という処理も,実際にみてみると,空いているメモリ空間をfor文(!)で探し見つけたらbreakして,というような実装になっている.

MMU

Memory Management Unitは,(この本の環境である)UNIX V6及びPDP-11/40という環境下においては,PAR(Page Address Register)とPDR(Page Description Register)という2つ1組のレジスタによって実現されている. PARは12bitのベースアドレスの情報を保持し,PDRは14bitの様々な情報を保持している.このPAR & PDRはカーネルモード用とユーザーモード用にそれぞれ8個,計16個用意されている. これは実行中のプロセスのみに使用されて,実行されていないプロセスにおいては,user構造体の所定の位置にこのレジスタの値が保存される.

簡単に言い換えると,仮想アドレスと物理アドレスを変換してくれる,ブラックボックスだと思えばいい.

システムコール

システムコールは,ユーザープログラム中で,特殊な権限をもつ命令を安全に実行できるように用意された,いわばOSが提供しているAPIのようなもの.

標準出力にprintする際にも,内部では,システムコールが発行されている.

システムコールがユーザープログラム中から呼ばれると,プロセッサのモードがカーネルモードに切り替わり,処理が終わると,制御がユーザープログラム側に戻ってくる.

基本的に重要な処理の場合が多いため,それをAPIとして隠蔽することで,安全性を高めている.

プロセスの切り替え

プロセスは,割り込みなどの処理により,カーネルによって,切り替わえられる..

実際に,プロセスの切り替えを実行している関数はswtch()という関数から始まる一連の処理だが,このとき,どのようにプロセスが切り替わるのか.また,その際に,実行中のプロセスの情報はどこに保持されるのか.

swtch()関数:https://minnie.tuhs.org//cgi-bin/utree.pl?file=V6/usr/sys/ken/slp.c

/*
 * This routine is called to reschedule the CPU.
 * if the calling process is not in RUN state,
 * arrangements for it to restart must have
 * been made elsewhere, usually by calling via sleep.
 */
swtch()
{
    static struct proc *p;
    register i, n;
    register struct proc *rp;

    if(p == NULL)
        p = &proc[0];
    /*
    * Remember stack of caller
    */
    savu(u.u_rsav);
    /*
    * Switch to scheduler's stack
    */
    retu(proc[0].p_addr);

loop:
    runrun = 0;
    rp = p;
    p = NULL;
    n = 128;
    /*
    * Search for highest-priority runnable process
    */
    i = NPROC;
    do {
        rp++;
        if(rp >= &proc[NPROC])
            rp = &proc[0];
        if(rp->p_stat==SRUN && (rp->p_flag&SLOAD)!=0) {
            if(rp->p_pri < n) {
                p = rp;
                n = rp->p_pri;
            }
        }
    } while(--i);
    /*
    * If no process is runnable, idle.
    */
    if(p == NULL) {
        p = rp;
        idle();
        goto loop;
    }
    rp = p;
    curpri = n;
    /*
    * Switch to stack of the new process and set up
    * his segmentation registers.
    */
    retu(rp->p_addr);
    sureg();
    /*
    * If the new process paused because it was
    * swapped out, set the stack level to the last call
    * to savu(u_ssav).  This means that the return
    * which is executed immediately after the call to aretu
    * actually returns from the last routine which did
    * the savu.
    *
    * You are not expected to understand this.
    */
    if(rp->p_flag&SSWAP) {
        rp->p_flag =& ~SSWAP;
        aretu(u.u_ssav);
    }
    /*
    * The value returned here has many subtle implications.
    * See the newproc comments.
    */
    return(1);
}

これを解決する仕組みがコンテキストスイッチである.コンテキストスイッチは,各レジスタが保持している値を,切り替えられるプロセスのuser構造体に退避させる.その逆も同じで,プロセスが再開されるとき,user構造体からレジスタの値を復帰し,プロセスが切り替えられる直前の状態を復元する.

レジスタには,プログラムカウンタなどの情報も含まれるため,退避と復元が可能になっている.これらの処理は,savu(),retu(),aretu()という関数に実装されている. savu()がuser構造体への保存,retu(),aretu()が,値の復元を担当する.

savu(),retu(),aretu():https://minnie.tuhs.org//cgi-bin/utree.pl?file=V6/usr/sys/conf/m40.s

_savu:
    bis $340,PS
    mov (sp)+,r1
    mov (sp),r0
    mov sp,(r0)+
    mov r5,(r0)+
    bic $340,PS
    jmp (r1)

_aretu:
    bis $340,PS
    mov (sp)+,r1
    mov (sp),r0
    br  1f

_retu:
    bis $340,PS
    mov (sp)+,r1
    mov (sp),KISA6
    mov $_u,r0

感想

自分の中で整理するためにまとめてみたが,正直本を読んでもらった方が早い気がする.

とてもいい本で,内容も重すぎない割に,得るものが非常に多い.

一個難点があるとすれば,古いプロセッサなので,実際に動かしたりできないという点. UNIX V6がx86で動くように調整されたxv6というOSもあるらしいので,時間がある時に触ってみようと思う.

参考:xv6 を読む

KVM/QEMU上の仮想マシンに対して,libvmiを使ってみる

環境

やっていくこと

  • libvmiとは
  • KVM/QEMUとは
  • VirtualBoxでNested Virtualizationはできない
  • 仮想化でない(USBブートした)Debianの上で,KVM/QEMUを動かす.
  • libvmiのサンプルプログラムを実行.

libvmiとは

libvmiとは,virtual machine introspectionの略である.

monitor the low-level details of a running virtual machine

このライブラリは,実行中の仮想マシンに対して,低いレイヤー,メモリの状態や,プロセスの状態などを監視することを実現するためのAPIを提供しているライブラリである.

サンプルコードも提供されており,これを実行することで,大雑把な動きを理解することができる.

対応している仮想化のプラットフォームは,KVM/QEMU及び,Xen

今回の記事では実際にこれを実行して結果を得られるところまで書く.

KVM/QEMUとは

もともと,この記事で扱っているテーマは,自分が今期やっていく研究の準備として行ったものである.今までは,VirtualBoxとDocker以外での仮想化をやったことがなかった.

KVM/QEMUという単語すら聞いたことない状態でのスタートだったため,まずはこれらの前提知識を調べるところから始まった.libvmiがどこでどのように動いているものなのかを大雑把に把握するためにも,KVM/QEMUについて簡単にまとめる.

KVMとは,Linuxカーネルに実装された仮想化の方法である.この機能を使うためにはいくつか条件がある.詳細については,後々解説する.

マシンの構成は,主にプロセッサ,メモリ,I/Oがあるが,KVMはこれらのうち,プロセッサの仮想化を担当している.

KVMは単体では動作せず,QEMUと組み合わせて使う.QEMUは,このアーキテクチャにおいては,メモリとI/Oを担当している.(QEMU自身でプロセッサの仮想化をすることもできるが,遅い)

単語の関係性として,仮想化の手段として,VirtualBoxと,KVM/QEMUQEMUは並列に考えることができる.その中で,今回はlibvmiを動かすために,KVM/QEMUという方法を選択するというだけ.

また,これらはVirtualBoxのように仮想マシンGUIで簡単に操作・管理できるソフトウェアが存在しない(正確に言えば普通にあるが,名前が違い,それらの関係性を把握するのも初見だと難しいので,下で解説)

virt-manager/virshとは

virt-managerとは,KVM/QEMUで起動している仮想マシンを操作・作成・管理などを行えるソフトウェアである.VirtualBoxは,KVM/QEMUのような,実際に仮想化を担当する部分と,virt-managerのようにそれらを管理する機能を一つのソフトウェアとしてまとめたものであると考えることができる.

virshは,あまりちゃんと使ってないのでうまく説明ができないが,これらの管理をシェルで実行できるようにしたもの,というイメージが正しそう.

関係性をシンプルにまとめた図として,以下のページの画像が非常に参考になる.

Virtual Machine Manager で VM を管理する」の「図 1. QEMU を使用した virt-manager スタックの概略図」

VirtualBoxでNested Virtualizationはできない

今回の環境は,仮想化されたLinuxではなく,USBブートしたLinux上としている.

これはKVMが動くかどうかに依存しており,結論からいうとVirtualBoxの上では,KVMは動作しない

引用になってしまうが,

  • 仮想化支援機能(Intel VTもしくはAMD-V)を搭載したプロセッサ
  • プロセッサの仮想化支援機能に対応したBIOSを持つマザーボード
  • ホストOS,ゲストOSを実行するために十分な容量のシステムメモリ
  • 64bit対応プロセッサを強く推奨(ディストリビューションによっては32bit非対応)
  • Intel EPT/AMD RVI搭載プロセッサの利用を強く推奨

これらのうち,Intel VTを搭載したプロセッサというのが,ハマるポイントになりうる.自分の調査不足かもしれないが,VirtualBoxで起動したマシンでは,プロセッサはこの機能を搭載していない(ように見える).

これをチェックする方法は以下.

cat /proc/cpuinfo | grep vmx
# flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch epb invpcid_single kaiser tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap intel_pt xsaveopt dtherm ida arat pln pts
# etc...

このコマンドを叩いて,出力が得られれば,とりあえずKVMは動くという認識.

VirtualBox上のDebianでは出力を得られなかった,

よって,USBブートという選択をした.研究用だし壊れたら作り直せばいい

Debianの上で,KVM/QEMUを動かす.

では,実際にこれらを動かすために必要なツールを入れていく.

前提として必要なツール

sudo apt install make automake gcc git vim

KVM/QEMU

sudo apt install -y qemu-kvm libvirt0 virt-manager bridge-utils qemu-system-x86
sudo reboot
sudo gpasswd libvirt -a debian # debianというユーザー名

これが入ったら,qemuのバージョンを確認しておく.

qemu-x86_64 --version
# qemu-x86_64 version 2.8.1(Debian 1:2.8+dfsg-6+deb9u4)
# Copyright (c) 2003-2016 Fabrice Bellard and the QEMU Project developers

この結果が,2.8.0以上だった場合,libvmiをビルドする前に少しコードをいじる必要がある.

libvirt

参考:libvmiで仮想マシン内部のプロセス一覧を取得...できなかった

apt installでこける場合は,一旦そのパッケージはスルー.

sudo apt install libyajl-dev libnl-3-dev libnl-route-3-dev libdevmapper-dev libpciaccess-dev
wget http://libvirt.org/sources/libvirt-3.0.0.tar.xz
xz -d libvirt-3.0.0.tar.xz
tar xf libvirt-3.0.0.tar
cd libvirt-3.0.0
./configure
make
sudo make install
sudo systemctl start libvirtd
sudo systemctl enable libvirtd

libvmi

参考:libvmiで仮想マシン内部のプロセス一覧を取得...できなかった

apt installでこける場合は,一旦そのパッケージはスルー.

sudo apt install kvm libtool m4 check bison flex libglib2-dev
git clone https://github.com/libvmi/libvmi.git
cd libvmi

デバッグを有効にする.

vim libvmi/debug.h
// 56行目付近のコメントアウトを外す
#define VMI_DEBUG __VMI_DEBUG_ALL

また,qemuのバージョンが2.8.0以上だった場合は,以下を編集

vim libvmi/driver/kvm/kvm.c
// 1482行目付近
// before
if (major >= 2 && minor >= 8) {
// after
if (major >= 2 && minor > 8) {

完了したら,ビルド.

./autogen.sh
./configure --enable-xen=no
export CPATH='/usr/include/glib-2.0:/usr/lib/x86_64-linux-gnu/glib-2.0/include:glib-2.0'
make
sudo make install
which vmi-dump-memory
# /usr/local/bin/vmi-dump-memory

この表示が出ればインストールは成功.

ISO

参考:Debian 9: 仮想化のKVMをインストールする

起動するDebianのISOファイルを引っ張ってきて,任意の場所に配置する.

D=https://cdimage.debian.org/debian-cd/current/amd64/iso-cd
wget ${D}/debian-9.4.0-amd64-netinst.iso
sudo mkdir /var/lib/libvirt/iso
sudo mv debian-9.4.0-amd64-netinst.iso /var/lib/libvirt/iso/
sudo chown libvirt-qemu:libvirt /var/lib/libvirt/iso/debian-9.4.0-amd64-netinst.iso
virt-manager
# or
sudo virt-manager
# を実行すると,ソフトウェアが立ち上がる.

この記事に従って,先ほど作成した,/var/lib/libvirt/isoを表示されるディレクトリに追加し,マシンを作成していく.

自分の設定では,ストレージにあまり余裕がなかったので,以下の設定とした.

  • メモリ1024MB
  • コア数1
  • ストレージ14GB

これで作成を押すと,仮想マシンが作成できる.

libvmiのサンプルプログラムを実行

おそらくKVM/QEMUが立ち上がっていると思うので,以下のコマンドで確認し,実行する.

# ドメインを取得
sudo virsh list --all
#  Id    名前                         状態
# ----------------------------------------------------
#  3     debian9                        実行中
touch ~/memory_dump_1GB
sudo vmi-dump-memory debian9 ~/memory_dump_1GB # 時間がかかる

完了すると,およそ1GBのファイルができていると思うので,それを確認してみる

xxd ~/memory_dump_1GB

それっぽい文字列が大量に出てきたら,メモリダンプ成功.

結果

libvmiを動かすことができた.

参考文献

Common Lispの基礎的な文法5

あくまで自分用のメモ。

今後の実装の時に、簡単に振り返るための記事。

進めている本は、Land Of Lisp

Land of Lisp

Land of Lisp

前回の記事の続き。

blog.ishikawa.tech

少し順番がおかしいが、今回の記事では、structgeneric周りについてやっていく。これは、本では9章に書いてある内容。

struct

Lispにも構造体というデータ構造は存在する。

(defstruct person
    name
    age
    waist-size
    favorite-color)

上のように、構造体を定義できる。

この構造体を利用するには、以下のように、defparameterを用いる。

(defparameter *bob* (make-person :name "Bob"
        :age 35
        :waist-size 32
        :favorite-color "blue"))

ここで注目なのが、make-personpersonの部分は構造体に合わせて動的に生成される、という点。

動的すぎて、不安になる。

generic

この章で扱うのは、データをgenericに扱うためのもの

例えば、リストか配列に入っている数値のグループを足し合わせたい。

こう言った際に、同じ機能のものを型の違い、つまりデータの取り出し方の違いだけで、複数書かないとダメなのか。となる。

当然そんなことはなく、lispには値をgenericに扱うための機構がいくつも用意していある。

例として、ジェネリックライブラリ関数、型述語、defmethod、ジェネリックアクセサなど。

これらの機能を使うと、defstructで作られたユーザー定義型に関しても、一つのコードで対応できるようになる。

まず、引数の型によらないコードを書くことを考えるが、この場合は、データのチェックを他の誰かにやってもらうのが楽。

そこで、もっともよく使われるのは、シーケンス関数

そのうちの一つが、length関数。 これはいろんな型を取れる。

これはどういうことかというと、型によって、処理を書き分けることができるようになる、ということ。

例えば、listが渡ってきた場合はその要素数を返すが、文字列が渡ってきた場合、文字数を返す、など。

reduce関数

これらのシーケンス関数の中でも、特に便利なものの一つがreduce関数。

(reduce #'+ '(3 4 6 5 2))

;; これあんまり意味がわからん ;; lambdaに、リストを渡している

(reduce (lambda (best item)
        ;; 偶数で、かつ、item > bestだった場合
        (if (and (evenp item) (> item best))
            item
            best))
    '(7 4 6 5 2)
    :initial-value 0)

(defun sum (lst)
    (reduce #'+ lst))

(sum '(1 2 3))

(sum (make-array 5 :initial-contents '(1 2 3 4 5)))

map

mapは、今までのmapcarとほぼ同じ動作をする。

違いは、前者は、いろいろ受け取れるのに対して、mapcarは、リストしか扱えない

(map 'list
    (lambda (x)
        (if (eq x #\s)
            #\S
            x))
    "This is a string")

Common Lispの基礎的な文法4

あくまで自分用のメモ。

今後の実装の時に、簡単に振り返るための記事。

進めている本は、Land Of Lisp

Land of Lisp

Land of Lisp

前回の記事の続き。

blog.ishikawa.tech

今回の記事では、lambdalist周りについてやっていく。

lambda

lambdaを使うと、名前を与えずに関数を作れる。

例えば、渡した数を半分にする関数。

(defun half(x)
    (/ x 2))

;; これを埋め込む。これは、halfという関数を、無名化したもの。
(lambda (n) (/ n 2))

;; これを実行すると、以下のようになる。
(mapcar (lambda (n) (/ n 2)) '(2 4 6))
;; (1 2 3)

これがマクロ

lambdaはlispの中で特に大事らしい。

説明がかなり細かく書いてあったが、今の自分にはその重要性を身を以て実感することができていないため、ここでは省略する。

List

Lispにおいて、Listは、Linked Listのような構造をしている。

前にも書いたかもしれないが、リストは、その一個一個がコンスセルとなっている。

一個のコンスセルには、2つの要素がある。

一個目は、その値自身。二個目は、次の要素へのアドレスのようなもの。

2個目の要素がnilだった場合、その要素はリストの終端を意味している。

また、最後の要素を、nilではなく、リストの先頭に指定することもできる(循環リスト)

しかし、これは要素数が無限になるので、そのままだとREPLが混乱する。

これを避けるために以下の手続きをとる。

;; 通常のList
(cons 1 (cons 2 (cons 3 nil)))
;; (1 2 3)


(defparameter foo (list 1 2 3))

;; 末尾の情報、本来nilになるべきところを、先頭の要素に向ける。
;; これをする際は、以下のコマンドを叩いて、REPLにそういうことをすることを知らせておかないといけない。
(setf *print-circle* t)

(defparameter foo (list 1 2 3))
;; 最後の要素に、fooを代入
(setf (cdddr foo) foo)

;; 要は、(1 2 3)というのは、
(cons 1 (cons 2 (cons 3 nil)))
;; となっていて、そのnilに、fooを代入しているということ

alist

コンスセルから作られるもののデータ構造の中でも、特に便利なのは、連想リスト、別名alistと呼ばれるもの。

例えば以下の例では、それぞれの人のコーヒーの注文を表している。

(defparameter *drink-order* '(
        (bill . double-espresso)
        (lisa . small-drip-coffee)
        (john . medium-latte)))

このalistから、要素を探すには、assocを使う。

(assoc 'lisa *drink-order*)
;; (LISA . SMALL-DRIP-COFFEE)

(push '(lisa . large-mocha-with-whipped-cream) *drink-order*)

(assoc 'lisa *drink-order*)
;; (LISA . LARGE-MOCHA-WITH-WHIPPED-CREAM)

また、assocでは、最初の一つの要素のみを、見つけてくる。(上の例でわかる)

今回は、以上。

次回は、グラフを作る処理の要点をまとめる。