目次: Linux
LinuxのI/O統計情報(/proc/[pid]/io)はユーザーが自分と一致するプロセスなら基本的に読めるはずですが、1つだけ読めない変なプロセスがいることに気づきました。
$ LANG=C ls -l /proc/*/io | grep $USER | cut -d ' ' -f 9 | xargs cat > /dev/null
cat: /proc/1381/io: 許可がありません
cat: /proc/1744137/io: そのようなファイルやディレクトリはありません
cat: /proc/1744138/io: そのようなファイルやディレクトリはありません
cat: /proc/1744139/io: そのようなファイルやディレクトリはありません
$ ps 1381
PID TTY STAT TIME COMMAND
1381 ? Ss 0:00 /usr/lib/systemd/systemd --user
$ ps u 1381
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
katsuhi+ 1381 0.0 0.0 21512 11360 ? Ss 2024 0:00 /usr/lib/syst
$ /usr/lib/systemd/systemd --version
systemd 257 (257~rc3-1)
+PAM +AUDIT +SELINUX +APPARMOR +IMA +IPE +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBCRYPTSETUP_PLUGINS +LIBFDISK +PCRE2 +PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD +BPF_FRAMEWORK +BTF -XKBCOMMON -UTMP +SYSVINIT +LIBARCHIVE
プロセスID 1744137〜1744139は、/proc/[pid]/ioの列挙に使ったls, grep, cutが、xargs catを実行するときに終了しているためで、気にしなくて良いはずです。しかしプロセスID 1381のsystemd --userは何かおかしいです。自分のプロセスのはずなのに読み出せなくてEPERMが返ります。
不思議ですね。ファイルを読み出せるかどうかチェックするaccess()を使って調べましょう。
#include <stdio.h>
#include <unistd.h>
void usage(int argc, char *argv[])
{
printf("usage:\n"
" %s filepath [filepath2 ...]\n", argv[0]);
}
const char *is_ok(int v)
{
if (v == 0) {
return "ok";
} else {
return "ng";
}
}
int main(int argc, char *argv[])
{
if (argc < 2) {
usage(argc, argv);
return 1;
}
for (int i = 1; i < argc; i++) {
const char *path = argv[i];
int fok, rok, wok, xok;
fok = access(path, F_OK);
rok = access(path, R_OK);
wok = access(path, W_OK);
xok = access(path, X_OK);
printf("%20s: f:%s r:%s w:%s x:%s\n",
path, is_ok(fok), is_ok(rok), is_ok(wok), is_ok(xok));
}
return 0;
}
$ gcc -O2 main.c -o access_check
$ ./access_check /proc/1381/io
/proc/1381/io: f:ok r:ok w:ng x:ng
$ cat /proc/1381/io
cat: /proc/1381/io: 許可がありません
ファイルが存在し(f:ok)、読み出しが可能である(r:ok)と判定されますが、catで読もうとするとやはりEPERMが返されます。なんだこれは……?
この記事にコメントする
目次: Linux
Linuxは各プロセスがどれくらいI/Oを行ったか記録していて、procファイルシステムの/proc/[pid]/ioファイルから読み出すことができます。
各フィールドの意味についてはUbuntuのマニュアル(Ubuntu Manpage: proc - プロセスの情報を含む疑似ファイルシステム)が日本語でも読めるしわかりやすいです。
(catを起動する、pidは1690787) $ cat /proc/1690787/io rchar: 3980 wchar: 0 syscr: 9 syscw: 0 read_bytes: 0 write_bytes: 0 cancelled_write_bytes: 0 (catにaとEnterを入力する) $ cat /proc/1690787/io rchar: 3982 wchar: 2 syscr: 10 syscw: 1 read_bytes: 0 write_bytes: 0 cancelled_write_bytes: 0
例としてaとEnterをcatに入力してみました。読み出し側を見てみると、rcharが2増えているのでaと改行文字の2バイトを、syscrが1増えているので1回のread()システムコールで読み出しているのでしょう。read_bytesが増えていないところを見ると、ファイルではなく端末から読み出したことも推測できます。
書き込み側はwcharが2増えているのでaと改行文字の2バイトを、syscwが1増えているので1回のwrite()システムコールで端末に書き出したと推測できます。読み込み側と異なり、書き込み側はファイルシステム層に書き出したかどうかは不明な仕様です。
最近のUbuntuやDebianであればデフォルト有効ですが、わざと無効にしているシステムもあるので有効にする方法を紹介しておきます。
LinuxカーネルのCONFIG_TASK_IO_ACCOUNTINGを有効にすると使用できます。CONFIG_TASK_XACCT、CONFIG_TASKSTATSに依存しているので合わせて有効にする必要があります。menuconfigから有効にする場合は下記の場所にあります。
General setup --->
CPU/Task time and stats accounting --->
[*] Export task/process statistics through netlink
[*] Enable extended accounting over taskstats
[*] Enable per-task storage I/O accounting
ちなみにx86_64向けではデフォルトコンフィグarch/x86/configs/x86_64_defconfigで太古の昔、2008年くらい(2.6.30くらいの時代)から有効になっています。
この記事にコメントする
目次: ベンチマーク
前回はループ、再帰なし、1000バイト以下で100万回のHello, World!を実施する問題に対し、バイナリサイズを104バイトまで削るためのアイデアと実装方法をご紹介しました。
100万回のHello, World!プログラムの方は所定の範囲に収まって動作しているので、特に変えなくて良いです。気になるとすれば、プログラムの終了ステータスがエラー(今は60)になっている程度です。原因はexitシステムコールに渡す引数が0ではないからで、syscall命令を呼ぶ前にrdiを0にすれば直ります。まあ、できたら良いな程度で動作には関係ありません。
Linuxのシステムコール呼び出し規約は下記のようになっています。
| syscall num | return | arg1 | arg2 | arg3 | arg4 | arg5 | arg6 |
|---|---|---|---|---|---|---|---|
| rax | rax | rdi | rsi | rdx | r10 | r8 | r9 |
バイナリファイルのサイズをこれ以上短くしようとするなら、ELFヘッダとプログラムヘッダをさらに重ねる必要があります。ELFヘッダとプログラムヘッダを完全に重ねると64バイト(2020年7月5日の日記参照)になりますが、プログラムはSegmentation Faultになってしまって動作しませんから、動作可能な重ね方を探す必要があります。
この記事にコメントする
目次: ベンチマーク
前回はループ、再帰なし、1000バイト以下で100万回のHello, World!を実施する問題に対し、バイナリサイズを112バイトまで削るためのアイデアと実装方法をご紹介しました。今まで112バイトが限界だと思っていましたが、とあるサイト(Tiny ELF Files: Revisited in 2021)からいくつかヒントを得て104バイトにできました。
サイズ104バイトの実行バイナリはこんな感じです。
$ hexdump -C 104byte.out 00000000 7f 45 4c 46 bb 40 42 0f 00 6a 01 58 89 c7 90 68 |.ELF.@B..j.X...h| -> ELF header 00000010 02 00 3e 00 b2 0e eb 10 04 00 3e 00 00 00 00 00 |..>.......>.....| -> ELF header 00000020 30 00 00 00 00 00 00 00 5e 83 c6 46 0f 05 eb 30 |0.......^..F...0| -> ELF header 00000030 01 00 00 00 05 00 38 00 01 00 00 00 00 00 00 00 |......8.........| -> ELF, Program header 00000040 01 00 3e 00 00 00 00 00 48 65 6c 6c 6f 2c 20 57 |..>.....Hello, W| -> Program header 00000050 6f 72 6c 64 21 0a 00 00 6f 72 6c 64 21 0a 00 00 |orld!...orld!...| -> Program header 00000060 ff cb 75 a5 6a 3c eb a3 |..u.j<..| -> Program header 00000068 $ ./104byte.out | head Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! $ ./104byte.out | wc 1000000 2000000 14000000
主な更新点は2つです。
順番に説明したいと思います。
前回はELFヘッダとプログラムヘッダを8バイト重ねましたが、さらに+8バイトつまり16バイト重ねることができます。
| バイナリ位置 | ELFヘッダ | プログラムヘッダ |
|---|---|---|
| 0x30 | e_flags: 何でもOK | p_type: 0x0000_0001 |
| 0x34 | e_ehsize: 何でもOK | p_flags: 下位が0x0005(RX)、上位は何でもOK |
| 0x36 | e_phentsize: 0x0038 | |
| 0x38 | e_phnum: 0x0001 | p_offset_l: 0x0000_0001 |
| 0x3a | e_shentsize: 何でもOK | |
| 0x3c | e_shnum: 何でもOK | p_offset_h: 0x0000_0000 |
| 0x3e | e_shstrndx: 何でもOK |
今までp_type, p_flagsとe_flags〜e_phentsizeは重ねられないと思っていましたが、p_flagsは下位が0x0005(Read, Executable)であれば上位ワードは何でも良く、うまく重ねられることを知りました。いやーこれはすごい。この工夫によってELFヘッダ + プログラムヘッダのサイズが104バイトになります。

値を変更できない部分(黄色: ELFヘッダ由来、緑色: プログラムヘッダ由来)
今回のバイナリで自由に変更してはいけない部分に色を塗るとこんな感じです。黄色がELFヘッダに由来する制約、緑色がプログラムヘッダに由来する制約です。
ちなみにp_flagsにWriteをつけるとSEGVでクラッシュして、カーネルがこんなエラーログを出します。
__vm_enough_memory: pid: 764111, comm: 104byte.out, bytes: 11138535030784 not enough memory for the allocation
Write属性をつけると書き込み用のメモリを確保しようとするのだと思われます。プログラムヘッダのp_fileszやp_memszにめちゃくちゃな値を指定しているため、そんなサイズは確保できずにエラーになります。従ってp_flagsは0x7(RWX)でなく0x5(RX)が必須です。
プログラムヘッダのp_vaddr(オフセット0x40)とp_paddr(オフセット0x48)は同じ値でなければならないと勘違いしていましたが、実はp_paddrはどんな値でも動作することを知りました。ELFとプログラムヘッダの重ね合わせで失われた8バイトを挽回しうる空き地となるでしょう。
ELFヘッダやプログラムヘッダの制約をリストアップするとこんなところです。
前回はC言語の配列で作っていましたが、バイナリコードに変換するのが面倒くさいので最初からアセンブラで書きます。アセンブラ実装はトリッキー度合いが低い方(2024年2月26日の日記参照)を流用しました。
.intel_syntax
.globl _start
.set _top, 0x3e0000
//e_ident
.byte 0x7f, 'E', 'L', 'F'
_start:
mov %ebx, 1000000
_loop:
push 0x01
_last:
pop %rax
mov %edi, %eax
nop
//push imm inst
.byte 0x68
//e_type
.word 0x0002
//e_machine
.word 0x003e
_second:
mov %dl, 0x0e
jmp _third
//e_entry
.quad _top + 4
//e_phoff
.quad 0x30
_third:
pop %rsi
add %esi, 0x46
syscall
jmp _fourth
//e_flags(any), p_type(0x01)
.long 0x01
//e_ehsize(any), p_flags low(0x05)
.word 0x05
//e_phentsize(0x38), p_flags high(any)
.word 0x38
//e_phnum(0x01), e_shentsize, e_shnum, e_shstrndx, p_offset(0x01)
.quad 0x01
//p_vaddr
.quad _top + 1
//p_paddr(any)
.byte 'H', 'e', 'l', 'l', 'o', ',', ' ', 'W'
//p_filesz
.byte 'o', 'r', 'l', 'd', '!', 0xa
.word 0
//p_memsz
.byte 'o', 'r', 'l', 'd', '!', 0xa
.word 0
//p_align(any)
_fourth:
dec %ebx
jne _loop
push 0x3c
jmp _last
トリッキーなことはせず素直に詰め替えましたが、それでも104バイトに収まりました。いいね〜。
$ as 104byte.asm -o 104byte.o $ ld --oformat binary 104byte.o -o 104byte.out $ ./104byte.out | wc (略) $ ./104byte.out | head (略) $ objdump -D 104byte.o -M intel (Intel記法で逆アセンブルする方法です、デフォルトはAT&T記法)
ビルド方法はこんな感じです。
今回はHello, World!の出力にwriteシステムコールを使っていて、rsiレジスタに文字列の先頭アドレスを渡す必要があります。単純にmov命令でesiレジスタに32bit即値をセットすると、5バイト命令が必要で配置に大きな制約が生じますので、小さい命令に分割して配置を容易にする方法を考えます。
単純に分割するとpush 5バイト、pop 1バイト、add 3バイトですが、ELFヘッダ内で変更できない邪魔者であるe_typeとe_machineの前にバイト0x68を置いてpush 0x3e0002命令にしてしまえば、4バイト節約できます。pushした値は下位アドレス2なので文字列の先頭(0x3e0048)を指すため0x46をaddします。
//push imm inst
.byte 0x68
//e_type
.word 0x0002
//e_machine
.word 0x003e
_second:
mov %dl, 0x0e
jmp _third
//e_entry
.quad _top + 4
//e_phoff
.quad 0x30
_third:
pop %rsi //pushした0x3e0002をpop
add %esi, 0x46 //esiの下位を0x02から0x48へ
syscall
jmp _forth
...
もう一つのカギはプログラムヘッダのp_vaddrを0x3e0001にして、プログラムを0x3e0001にロードすることです。下位バイトが1なのはプログラムヘッダのp_offsetが1になっているせいです。
この記事にコメントする
| < | 2025 | > | ||||
| << | < | 02 | > | >> | ||
| 日 | 月 | 火 | 水 | 木 | 金 | 土 |
| - | - | - | - | - | - | 1 |
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | - |
25年10月15日
25年10月18日
22年5月5日
25年10月19日
23年4月11日
06年4月22日
25年10月17日
25年10月6日
25年10月13日
20年10月23日
25年10月12日
20年8月29日
19年1月13日
18年10月13日
18年9月3日
18年8月20日
18年7月23日
18年7月22日
18年10月14日
18年11月10日
wiki
Linux JM
Java API
2002年
2003年
2004年
2005年
2006年
2007年
2008年
2009年
2010年
2011年
2012年
2013年
2014年
2015年
2016年
2017年
2018年
2019年
2020年
2021年
2022年
2023年
2024年
2025年
過去日記について
アクセス統計
サーバ一覧
サイトの情報合計:
本日: