コグノスケ


link 未来から過去へ表示(*)  link 過去から未来へ表示

link もっと前
2025年2月10日 >>> 2025年1月28日
link もっと後

2025年2月10日

100万回のHello, World! - 補足

目次: ベンチマーク

前回はループ、再帰なし、1000バイト以下で100万回のHello, World!を実施する問題に対し、バイナリサイズを104バイトまで削るためのアイデアと実装方法をご紹介しました。

100万回のHello, World!プログラムの方は所定の範囲に収まって動作しているので、特に変えなくて良いです。気になるとすれば、プログラムの終了ステータスがエラー(今は60)になっている程度です。原因はexitシステムコールに渡す引数が0ではないからで、syscall命令を呼ぶ前にrdiを0にすれば直ります。まあ、できたら良いな程度で動作には関係ありません。

Linuxのシステムコール呼び出し規約は下記のようになっています。

syscall numreturnarg1arg2arg3arg4arg5arg6
rax rax rdi rsi rdx r10 r8 r9

バイナリファイルのサイズをこれ以上短くしようとするなら、ELFヘッダとプログラムヘッダをさらに重ねる必要があります。ELFヘッダとプログラムヘッダを完全に重ねると64バイト(2020年7月5日の日記参照)になりますが、プログラムはSegmentation Faultになってしまって動作しませんから、動作可能な重ね方を探す必要があります。

編集者:すずき(2025/02/12 00:32)

コメント一覧

  • hdkさん(2025/02/12 08:01)
    なるほど、最後に%rdiを0にするのはこれでいけますね!
    --ADDRS----F1---F2-
    0000000A 01 3C
    0000000B 58 6A
    0000000C 89 01
    0000000D C7 58
    0000000E 90 50
    00000028 90 5F
    00000063 A5 A7
    00000064 6A 58
    00000065 3C 53
    00000067 A3 A7
  • hdkさん(2025/02/12 08:06)
    あ、すみません、比較元に間違いがありました...
    0000000F 68 BE
    00000028 5E 5F
  • すずきさん(2025/02/13 02:03)
    解読しました。なるほど、exitの引数が0になっていい感じです〜。

    _start:
    mov %ebx, 1000000
    push 0x3c

    _loop:
    push 0x1
    pop %rax
    push %rax

    _last:
    //mov esi,imm inst
    .byte 0xbe
    .word 0x0002
    .word 0x003e

    _second:
    mov %dl, 0x0e
    jmp _third

    _third:
    pop %rdi
    add %esi, 0x46
    syscall
    jmp _fourth

    _fourth:
    dec %ebx
    jne _loop
    pop %rax
    push %rbx
    jmp _last
open/close この記事にコメントする



2025年2月7日

100万回のHello, World! - バイナリサイズを削って遊ぼう、112バイトから104バイトへ

目次: ベンチマーク

前回はループ、再帰なし、1000バイト以下で100万回のHello, World!を実施する問題に対し、バイナリサイズを112バイトまで削るためのアイデアと実装方法をご紹介しました。今まで112バイトが限界だと思っていましたが、とあるサイト(Tiny ELF Files: Revisited in 2021)からいくつかヒントを得て104バイトにできました。

概要

サイズ104バイトの実行バイナリはこんな感じです。

100万回のHello, World!(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→16バイトに拡大
  • p_vaddr == p_paddrは不要でp_addrは何でも良い

順番に説明したいと思います。

ELFヘッダとプログラムヘッダを重ねる

前回はELFヘッダとプログラムヘッダを8バイト重ねましたが、さらに+8バイトつまり16バイト重ねることができます。

バイナリ位置ELFヘッダプログラムヘッダ
0x30e_flags: 何でもOK p_type: 0x0000_0001
0x34e_ehsize: 何でもOK p_flags: 下位が0x0005(RX)、上位は何でもOK
0x36e_phentsize: 0x0038
0x38e_phnum: 0x0001 p_offset_l: 0x0000_0001
0x3ae_shentsize: 何でもOK
0x3ce_shnum: 何でもOK p_offset_h: 0x0000_0000
0x3ee_shstrndx: 何でもOK

今までp_type, p_flagsとe_flags〜e_phentsizeは重ねられないと思っていましたが、p_flagsは下位が0x0005(Read, Executable)であれば上位ワードは何でも良く、うまく重ねられることを知りました。いやーこれはすごい。この工夫によってELFヘッダ + プログラムヘッダのサイズが104バイトになります。


値を変更できない部分(黄色: ELFヘッダ由来、緑色: プログラムヘッダ由来)

今回のバイナリで自由に変更してはいけない部分に色を塗るとこんな感じです。黄色がELFヘッダに由来する制約、緑色がプログラムヘッダに由来する制約です。

ちなみにp_flagsにWriteをつけるとSEGVでクラッシュして、カーネルがこんなエラーログを出します。

p_flagsに0x7(RWX)を指定した時のカーネルのエラーログ
__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_paddrは何でも良かった

プログラムヘッダのp_vaddr(オフセット0x40)とp_paddr(オフセット0x48)は同じ値でなければならないと勘違いしていましたが、実はp_paddrはどんな値でも動作することを知りました。ELFとプログラムヘッダの重ね合わせで失われた8バイトを挽回しうる空き地となるでしょう。

  • e_identの先頭4バイトは0x7f, 'E', 'L', 'F
  • e_type(オフセット0x10)は0x0002
  • e_machine(オフセット0x12)は0x003e
  • e_entry(オフセット0x18)は実行開始アドレス(エントリアドレス)を指す
  • p_filesz == p_memszが必要
  • p_fileszは何でもOKだが、大すぎる値はNG(上位2バイトは0じゃないとSegmentation Faultする)

ELFヘッダやプログラムヘッダの制約をリストアップするとこんなところです。

実装

前回はC言語の配列で作っていましたが、バイナリコードに変換するのが面倒くさいので最初からアセンブラで書きます。アセンブラ実装はトリッキー度合いが低い方(2024年2月26日の日記参照)を流用しました。

104バイト版の実装

	.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します。

e_typeとe_machineをpush命令にしている箇所

	//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/10 02:45)

コメント一覧

  • hdkさん(2025/02/10 22:31)
    push $0x3e0002のかわりにmov $0x3e0002,%esiにしてよくてそうするとpop %rsiをnopにできるんですが、空いたスペースの活用が難しく、push 0x3cをプログラム先頭に持っていってmov %eax,%ediをpush;popに分解すると、ふたつのnopのところにうまく埋まって2バイト縮むなぁと思ったら、実際に縮めたらエラーになって起動できませんでした。残念...
  • すずきさん(2025/02/12 00:11)
    なるほど。動きそうなのになんで動かないんだろう……。
    もっと短くできそうではあるんですけど、これ以上ELFヘッダとプログラムヘッダを重ねる方法が見つからない限りは、縮めてもnopが増えるだけでメリットがないんですよねえ。
open/close この記事にコメントする



2025年1月31日

Windowsの日本語フォントはみな長生き

GNOME48からフォントが変わるニュースを見ていて、フォントを作成するのが割と簡単な英語圏にしては、Cantarellを15年も使っていたのは珍しい?んでしょう。結構息が長かったんだなあと思います。

フォントが長く使われる傾向に関しては日本語フォントもたいがいだよなあ?と思ったのでWindowsの日本語フォント変遷をリストアップしてみました。

  • MSゴシック: Windows 3.1(1993〜)
  • MS Pゴシック: Windows 95(1995〜)
  • MS UIゴシック: Windows 98(1998〜)
  • メイリオ: Windows Vista, 7, 8(2006〜)
  • 游ゴシック: Windows 10, 11(2015〜)

やはりWindowsの日本語フォントはみんな息が長いです。次世代にバトンを渡すまでの期間だけ見ても、MSゴシック13年、メイリオ9年、游ゴシックも10年経ったんですね。Windowsの次バージョンが出るか出ないか定かじゃないですけど、もし次があったとしたらフォントを変えてきそうですね。

編集者:すずき(2025/02/03 23:21)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
2025年2月10日 >>> 2025年1月28日
link もっと後

管理用メニュー

link 記事を新規作成

<2025>
<<<02>>>
------1
2345678
9101112131415
16171819202122
232425262728-

最近のコメント5件

  • link 25年2月10日
    すずきさん (02/13 02:03)
    「解読しました。なるほど、exitの引数が...」
  • link 25年2月10日
    hdkさん (02/12 08:06)
    「あ、すみません、比較元に間違いがありまし...」
  • link 25年2月10日
    hdkさん (02/12 08:01)
    「なるほど、最後に%rdiを0にするのはこ...」
  • link 20年6月29日
    すずきさん (02/12 00:12)
    「お役に立ったようであれば幸いです。」
  • link 25年2月7日
    すずきさん (02/12 00:11)
    「なるほど。動きそうなのになんで動かないん...」

最近の記事3件

  • link 21年5月22日
    すずき (02/12 00:33)
    「[ベンチマーク - まとめリンク] 目次: ベンチマーク一覧が欲しくなったので作りました。最速のyes不安定なyesyesの高...」
  • link 25年2月10日
    すずき (02/12 00:32)
    「[100万回のHello, World! - 補足] 目次: ベンチマーク前回はループ、再帰なし、1000バイト以下で100万...」
  • link 25年2月7日
    すずき (02/10 02:45)
    「[100万回のHello, World! - バイナリサイズを削って遊ぼう、112バイトから104バイトへ] 目次: ベンチマ...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 2025年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www2.katsuster.net
RDFファイル RSS 1.0

最終更新: 02/13 02:03