69
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

何がOS実装の理解を難しくしているのかとGNU assemblyの準備

Last updated at Posted at 2018-09-23

何がOS実装の理解を難しくしているのか

OS内部どうなってんのか知っといた方がいいんだろうなぁ、と思っている人向けになんか書きたいなぁ、と思って書きました。

OSを理解しようとすると、初回は挫折感をあじわって退場するのですが、原因いろいろあると思います。

  • C言語、アセンブリ(gnu assembly)、ELF, linker scriptの知識
  • boot処理時(の登場人物)で混乱する
  • マルチタスク処理(switching, interrupt)がわからん
  • OS自作しようぜ、みたいなはじめから強い本が多い

まぁ、最後のは冗談として(自分は凡人なので..)、 https://github.com/mit-pdos/xv6-public (Unix V6のx86(32bit) implementation。以下xv6と呼びます) と https://pdos.csail.mit.edu/6.828/2018/xv6.html がとてもよかったので、これをソースコードリーディングすることで、自分が詰まったand他人も詰まりそうなところをピックアップする感じで解きほぐしていきたい。

と思ったのですが、すべてのトピックを扱うと分量あまりに多くなったので、本記事では、枕の部分(モチベーションおよび、C言語、アセンブリ(gnu assembly)、ELF, linker scriptの知識、特に、GNU assemblyについては個別に)についてだけに言及し、本編はいくつかに分けました:

目次

OSの内部実装を知ってよかったこと、あるいはOSを学ぶモチベーション

得体の知れないものを「これ、いつか役立つんや~」って思いながら、自身の時間を割いてやるのは、精神衛生上よろしくないと思うので、最初に自分がOSの内部実装を知って一番よかったことを述べますと、
「ハードウェア今まで全然知らんかったなぁ」
と気づいたことです。

今まで自明だと思って動かしていたものが、ハードウェアとの連携でいろんな動作が実現されているのがわかり、実は全然混沌としていて自明でなかった:scream:、という気づきが何回もあり、視野がかなり広がったな、という感じがします。

あとは、プログラム動作時のメモリの配置のイメージがかなりはっきり見える感じがします。お給料あがるかどうかはちょっとよくわかりません。(上がれば:heart_eyes:なんですけどね~)

xv6のソースコードリーディングをおすすめする理由

自分自身、実ははじめてのOSコードリーディングという本を買った後でxv6の存在を知りました。「はじめてのOSコードリーディング」とは、UNIX V6という古のカーネルを題材にして挙動を理解しようというコンセプトの本です。その本によれば、初心者におすすめできる理由がいくつかあるとのことで、

  • 一万行のソースコードで全体が把握しやすい(linux kernelはその数千倍くらいあります)

  • ロジックがシンプル

  • OSの基本となるアイデアが詰まっている

が挙げられます。しかしながら、

  • 古のOSで、ANSI C以前のC言語らしきもので書かれており、若干文法が異なる。

  • プロセッサが古すぎ -> qemuとかで動作しない

  • 大部分の人がいままで触れたことのない、プロセッサ(PDP-11)の仕様書を参照する必要がある

という欠点があります。xv6はMITがUNIX v6をx86仕様に書き直した教育用OSで、上記の利点を受け継ぎつつ、

  • 普通のC言語(C99)で書かれている

  • Intel x86上(32bit)で動く(x86-64(64bit)とx86は多少の相違点はあれど、だいたい同じです) -> qemuなどのメジャーなエミュレータで動作する!

ということで、欠点が解消されています。また、解説も豊富で、

で十分に資料があります。仕様書を適宜参照するのもOSコードリーディングの際は非常に重要なのですが、referenceページにてすでにリンク集が用意されているので、仕様書を探す労力が省けます(この利点は非常に大きいと思います)

x86自体の資料については、

あたりが参考になります。(プロセッサ自体の仕様解説資料も大量にあるということを言いたかった)
ちなみに、Intelの公式のreference(Intel SDMと今後呼びます)はvol.1でなくvol.3を使用することがほとんどですので、お間違えなく。

あとは、自作するよりも(教育用でコメント付きの)ソースコードリーディングの方が消化不良に陥りにくいと思います。ということで、とっかかりにお勧めしたいです。

著者の力量について

ここは注釈みたいな感じなんですが、私自身は、Linux Kernel内部実装の解説、例えば、

などをまだほとんど読んでいません。そのため、xv6と比べてlinuxの内部実装はこうだ、ということを言い切る自信がありませんので、ご了承くださいませ。(linux programmingのThe Linux Programming Interfaceはほぼ読んだのでユーザーランドの方はだいたいわかっていると思ってる) とはいえ、ベースは似たようなものじゃないかと思っています。
ということでめっちゃ詳しい方がいらっしゃいましたら、変なこと言っていたら突っ込んでくださると幸いです。

2019/8追記) Understanding the Linux Kernel: From I/O Ports to Process Management(3rd Edition)についてはしっかり理解しつつ、完走しました。パット見たかんじ、この本が一番詳細に解説しているのですが、バージョンは2.6.11でかなり古いです。個人的には、この本によってlinux kernelの未知のコードも自力で読めるだけの力量をつけられたので、未だ読む価値はあるんじゃないか、と思っています。


ちょっと前置きが長かったですが、ここからが本題です。

C言語、アセンブリ(gnu assembly)もろもろの知識

C言語から説明するわけにも行かないので、いくつか本や参考となるページを紹介します。

  • OS開発に関しては、C言語をお勧めします。C言語実装がOS開発においては一番情報量多いですし、boot処理のプログラムはlinker scriptの知識も必要になってくるので、gcc使うことを考えると、スクラッチOS実装したければCが無難です。ポインタだけ曖昧なら、エキスパートCプログラミングがお勧めです。
          
    あとは、https://github.com/mit-pdos/xv6-public のコードがC実装なので。強くてニューゲームするときはGoでもRustでもなんでも良いんじゃないかと思います。

  • assemblyはnasmよりGNU assembly(gas)の方に慣れておきましょう。理由は上と関連しているのですが、gasの方がgccと相性が良いからです。あとは、両者、記法が結構違っていて(nasm: Intel記法、gas: AT&T記法)、例えば、srcとdstの位置が逆です。xv6ではgasです。OSのbootプログラムではふだんは使わないようなx86アセンブラ命令がたくさん出てくるのですが、その都度ググる感じで大丈夫です2。その他、重要なポイントについては次節で解説します。

  • unixプログラミングの知識に関しては、ふつうのLinuxプログラミングのfork, execあたりまでを理解していれば十分だと思います。知識を多く持っているに越したことはないって感じです。ちなみに、ここら辺を体系的に身につけたければ、The Linux Programming Interfaceが超おすすめです。

  • ELF(Executable and Linking Format) に関しては、http://www.cirosantilli.com/elf-hello-world および、https://qiita.com/knknkn1162/items/c67ae7c2ef71a713adf8 を読みましょう。手を動かして確かめるのを推奨します。用語集としてbinary hacksパラパラ見るのもお勧めです。

  • linker scriptに関しては、上のELFのリンクが十分に読めるなら、リンカ・ローダ実践開発テクニックはもう十分に読めるレベルになっているので、5章、6章あたりを読めばスッキリします。(が本記事で触れます)

  • その他の必要な部分に関しては、なるべく記事で確認していきます。

assemblyを読むポイント

基本的な事柄(レジスタやシステムコールとか)に関しては、x86_64 Linux Assemblyシリーズ(youtube)が良いかなと思いますので、そちらを参考にしてください。

Note) x86-64では%rax, %ripみたいな風になっていると思いますが、今回はx86(32bit)を見ていきますので、汎用レジスタはeax, ecx, edx, ebx, esp, ebp, esi, ediです。

本記事では、以下の点について解説していきたいと思います:

  • %espレジスタと%eipレジスタ
  • linker script

%espレジスタと%eipレジスタ

OS(xv6)内のassemblyのコードを読むためには、%espレジスタと%eipレジスタの正確な把握が非常に重要です。簡単にいうと、

  • %espレジスタは現在のスタックを指し示すポインタ(stack pointer)が格納されているレジスタ
  • %eipレジスタは次のinstructionへのポインタ(program counter)が格納されているレジスタ

で、これら二つはプログラム実行時に目まぐるしく変化するわけですが、これら二つのレジスタをうまいこと動かすことで、特にマルチタスク(switching, interruptともに)処理を実現させているからです。real modeからprotect modeに移行する際のjump でも%eipの動作が暗黙的に関わってきます。

とは言っても基本はそんなに難しくありませんので、Hello worldプログラムを参考に見ていきます。GNU assemblyでx86アーキテクチャです。ということで、環境構築の後、数種類のサンプルを見ていきます。

環境構築

32bit OSの環境を作っていきます。

Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/trusty32"
  config.vm.synced_folder ".", "/home/vagrant/shared"
end
$ vagrant up
$ vagrant ssh
$ cd shared
# gccはすでに入っているはず
$ sudo apt-get install gdb

なんのひねりもないHello world

hello0.S
.text
  .globl main

main:
  # sys_write(0x04)の呼び出し
  movl $0x04, %eax
  movl $0x01, %ebx # STDOUT_FILENO
  movl $message, %ecx
  movl $12, %edx # length
  int $0x80
  # sys_exit(0x01)の呼び出し
  movl $0x01, %eax
  xorl %ebx, %ebx
  int $0x80

message:
  .ascii "Hello world\n"
# -nostdlib: リンク時に標準システムスタートアップファイルや標準システムライブラリを使用しない
# -c: compileのみ
$ gcc -c -nostdlib  hello0.S
# -emain: entry pointをmainにする(defaultは`_start`)
$ ld -emain hello0.o
# もしくは、`gcc -nostdlib -Wl,-emain hello0.S`
$ ./a.out
Hello world
$ objdump -d ./a.out

./a.out:     file format elf32-i386


Disassembly of section .text:

08048054 <main>:
# 0x08048054はアドレス(正確にはvirtual memory address)です。
# 本当はsegmentationとpagingを理解する必要があるのですが、boot処理で詳しく説明します。
 8048054:   b8 04 00 00 00          mov    $0x4,%eax
 8048059:   bb 01 00 00 00          mov    $0x1,%ebx
 804805e:   b9 73 80 04 08          mov    $0x8048073,%ecx
 8048063:   ba 0c 00 00 00          mov    $0xc,%edx
 8048068:   cd 80                   int    $0x80
 804806a:   b8 01 00 00 00          mov    $0x1,%eax
 804806f:   31 db                   xor    %ebx,%ebx
 8048071:   cd 80                   int    $0x80

08048073 <message>:
  # Note: ここはobjdumpによって強引にinstructionに変換されている部分なので、48: 'H', 65: 'e', ...0a: '\n'に相当します。
 8048073:   48                      dec    %eax
 8048074:   65                      gs
 8048075:   6c                      insb   (%dx),%es:(%edi)
 8048076:   6c                      insb   (%dx),%es:(%edi)
 8048077:   6f                      outsl  %ds:(%esi),(%dx)
 8048078:   20 77 6f                and    %dh,0x6f(%edi)
 804807b:   72 6c                   jb     80480e9 <message+0x76>
 804807d:   64                      fs
 804807e:   0a                      .byte 0xa

さて、%esp%eipレジスタの動きを追います。レジスタの値の確認はgdbで行うのが良いでしょう。

gdbの操作については、http://d.hatena.ne.jp/murase_syuka/20150912/1442021005
各種コマンドについては、http://d.hatena.ne.jp/Watson/20100318/1268887029 がわかりやすいです。

hello0.Sをコンパイル、リンクしたものをgdbで動かすとこんな感じになります。(3つパネルがあると思いますが、特に1つめのパネルに注目してください。真ん中のパネルの背景白になっている部分は次に進むべきinstructionを指し示しています。)

sample.gif
https://asciinema.org/a/8sN1avb34nH3g4v78z7nBB78Y

%eipはinstructionの実行ごとに次々に変化していることが重要です。というのも、%eipは次のinstructionへのレジスタをさしているからです。(.text section内に%eipがあります。一方、%espは(下方向にスタックが積まれるので)stack領域のbottommostです)
一方で、%esp0bffff750で全く変化していません。%espはstack pointerと言ってスタック領域の最下部にあります。特にhello0.sのプログラムではスタックに積むべきものがないからです。

全体のmemory mapを俯瞰して見ると、以下のような感じになります:

スクリーンショット 2018-09-20 11.41.26.png
The Linux Programming Interface 2nd Ed.(Fig6.1)から引用

%eipレジスタは下図のText(program code)と書いているところの領域内のアドレスで、%espはtop of stackと書いてあるちょうどそのアドレスを指し示しています3

helloの関数化

hello_exit.S
.text
  .globl main

main:
  call hello
  call exit

hello:
  movl $0x04, %eax
  movl $0x01, %ebx
  movl $message, %ecx
  movl $12, %edx # length
  int $0x80
  ret

exit:
  # exit
  movl $0x01, %eax
  xorl %ebx, %ebx
  int $0x80

message:
  .ascii "Hello world\n"
$  gcc -c -nostdlib  hello_exit.S
$  ld -emain hello_exit.o
$ ./a.out
Hello world

$ objdump -d ./a.out

./a.out:     file format elf32-i386


Disassembly of section .text:

08048054 <main>:
 8048054:   e8 05 00 00 00          call   804805e <hello>
 8048059:   e8 17 00 00 00          call   8048075 <exit>

0804805e <hello>:
 804805e:   b8 04 00 00 00          mov    $0x4,%eax
 8048063:   bb 01 00 00 00          mov    $0x1,%ebx
 8048068:   b9 7e 80 04 08          mov    $0x804807e,%ecx
 804806d:   ba 0c 00 00 00          mov    $0xc,%edx
 8048072:   cd 80                   int    $0x80
 8048074:   c3                      ret

08048075 <exit>:
 8048075:   b8 01 00 00 00          mov    $0x1,%eax
 804807a:   31 db                   xor    %ebx,%ebx
 804807c:   cd 80                   int    $0x80

0804807e <message>:
 804807e:   48                      dec    %eax
 804807f:   65                      gs
 8048080:   6c                      insb   (%dx),%es:(%edi)
 8048081:   6c                      insb   (%dx),%es:(%edi)
 8048082:   6f                      outsl  %ds:(%esi),(%dx)
 8048083:   20 77 6f                and    %dh,0x6f(%edi)
 8048086:   72 6c                   jb     80480f4 <message+0x76>
 8048088:   64                      fs
 8048089:   0a                      .byte 0xa

gdbで追いかけるとこんな感じ:

1分くらいで長いので、こちらのリンクも。
https://asciinema.org/a/aD89Pre6R7bls6wplOxi2eb6r
sample.gif

call helloを呼んだ直後、%espが変更されていることがわかるかと思います。

# call helloを呼んだ直後
(gdb) x/8xw $esp                                                                                                      
0xbffff74c:     0x08048059      0x00000001      0xbffff87f      0x00000000                                            
0xbffff75c:     0xbffff8a8      0xbffff8b9      0xbffff8cd      0xbffff8dd    

stackはgrow downすることを踏まえると、0x08048059(call 804805e <hello>の次のinstruction)をスタックに積んで%espが0xbffff750から0xbffff74c(0xbffff750 - 4)に移動しているのが見て取れます。

hello 関数が終了した直後は

# hello 関数がretを呼んだ直後
(gdb) x/8xw $esp                                                                                                      
0xbffff750:     0x00000001      0xbffff87f      0x00000000      0xbffff8a8                                            
0xbffff760:     0xbffff8b9      0xbffff8cd      0xbffff8dd      0xbffff8fa 

となり、%esp0xbffff74cから0xbffff750に移動しています。実のところ、ret instructionがその役割を果たしていて、

  1. アドレス0x08048059を%eipにpopする。(%espはスタック領域+4移動する)
  2. アドレス0x08048059call 8048075 <exit>なので、そちらにジャンプする

みたいな挙動になっています。

Note) call関数のint $0x80が呼ばれた時点でプログラムが終了するため、call関数にはretをつけてません


以上、関数が呼ばれる仕組みは%eipと%espが重要な役割を果たしており、

  1. call helloを実行する
  2. call exit(main関数内のcall hello直後のaddress)をスタックにpushして、%espを4byte分(pointerのサイズ分)decrementしておく
  3. hello関数の先頭アドレスにjumpする
  4. %eipにhello関数の2番目のinstructionを指し示すaddressを格納
  5. (hello関数の実行)
  6. stack領域をpopして、call exit(call hello直後のaddress)を%eipに格納する。%espは4byte分(pointerのサイズ分)incrementされている
  7. %eipは次のinstructionへのaddressが格納されているregisterだったので、そのaddressにジャンプ
  8. call exitを実行する
  9. 2からの流れと同様...

となります。スタックに関数へのaddressをpush/popすることで、main -> hello -> mainに戻るということを可能にしています。

ret instructionの応用

前節に関連してですが、実戦で重要なこと。call instructionとretが対となって呼ばれる必要はありません。どう言うことかと言うと、ret

  1. アドレスXXXを%eipレジスタにpopする。(%espレジスタは現在の場所+4になる)
  2. アドレスXXXにジャンプする(コレは%eip本来の挙動)

のような挙動をするのですが、call instructionを使わずともこのアドレスXXXをstack領域にうまいことはめこめば、retが実行された時にどこでもjumpできると言うわけです。

例えば、

hello_exit2.s
.text
  .globl main

main:
  movl $exit, %eax
  pushl %eax
  jmp hello

hello:
  movl $0x04, %eax
  movl $0x01, %ebx
  # (.. 省略)

でも問題なく動きます。(このコード自体はかなり作為的ですが)

またまたgdbでの動作
sample.gif

https://asciinema.org/a/vxsM0hH2z9fvsoazTFBWPs9dY

先ほどと違うところは、

  1. pushl %eaxでstackにexit関数のaddressを積んでいるところ

  2. jmpでは次のinstructionへのアドレスをstackに積まない(callは次のinstructionへのアドレスをstackに積むのだった)

  3. hello関数のretの後、いきなり、exit関数の一番初めにjumpしている

ことです。1でstackに飛びたい関数へのポインタを積んでいるところがキモで、一見するとアクロバティックな飛び方をしています。 
今回の例だと、[main] -> [print] -> [mainに(戻るべきところを)戻らず、exit関数にジャンプ] しています。

つまり、アセンブリの世界では関数呼び出しは必ずしも入れ子でよばれるとは限らないのです。(ここは普通の(CとかPythonとか)プログラミングとは異なるかなり独特な部分です。)

なんで紹介したかと言うと、xv6(OS)でこれ相当のことをやっているところが、trapretの部分: (1番相当のコード -> 3番相当のコード )とか、execでentrypointが切り替わる部分: (1番相当のコード -> 3番相当のコード )とか色々あるからです。RUNNABLEになったプロセスからschedulerのプロセスに移る際も同じようなことをしているのですがさらに複雑ですので、別の記事で紹介します。

と言うことで、stackに飛びたい関数へのアドレスを突っ込むことで関数のジャンプが自由にできると言うお話でした。

引数を持つ場合

上のhelloの場合、messageもlengthも埋め込まれてしまっているので、hello worldしか出力できない関数で融通が効かないです。ということで、hello関数を抽象化して、2つの引数を持つprint関数を作ってみます:

hello_print.s
  .text
  .globl main

main:
  pushl $message
  pushl $size
  call print
  subl $8, %esp
  call exit

print:
  // save %ebp
  pushl %ebp
  // save current %esp address in %ebp as temporary value, because real %esp may change.
  movl %esp, %ebp
  movl 8(%ebp), %edx
  movl (%edx), %edx
  movl 12(%ebp), %ecx
  movl $0x04, %eax
  movl $0x01, %ebx
  int $0x80
  movl %ebp, %esp
  // restore %ebp
  popl %ebp
  ret
exit:
  addl $0x08, %esp
  movl $0x01, %eax
  xorl %ebx, %ebx
  int $0x80


message:
  .ascii "Hello world\n"
size:
  .long   size - message
$ gcc -c -nostdlib hello_print.s
$ ld -emain hello_print.o
$ ./a.out
$ objdump -d ./a.out

./a.out:     file format elf32-i386


Disassembly of section .text:

08048054 <main>:
 8048054:   68 92 80 04 08          push   $0x8048092
 8048059:   68 9e 80 04 08          push   $0x804809e
 804805e:   e8 08 00 00 00          call   804806b <print>
 8048063:   83 ec 08                sub    $0x8,%esp
 8048066:   e8 1b 00 00 00          call   8048086 <exit>

0804806b <print>:
 804806b:   55                      push   %ebp
 804806c:   89 e5                   mov    %esp,%ebp
 804806e:   8b 55 08                mov    0x8(%ebp),%edx
 8048071:   8b 12                   mov    (%edx),%edx
 8048073:   8b 4d 0c                mov    0xc(%ebp),%ecx
 8048076:   b8 04 00 00 00          mov    $0x4,%eax
 804807b:   bb 01 00 00 00          mov    $0x1,%ebx
 8048080:   cd 80                   int    $0x80
 8048082:   89 ec                   mov    %ebp,%esp
 8048084:   5d                      pop    %ebp
 8048085:   c3                      ret

08048086 <exit>:
 8048086:   83 c4 08                add    $0x8,%esp
 8048089:   b8 01 00 00 00          mov    $0x1,%eax
 804808e:   31 db                   xor    %ebx,%ebx
 8048090:   cd 80                   int    $0x80

08048092 <message>:
 8048092:   48                      dec    %eax
 8048093:   65                      gs
 8048094:   6c                      insb   (%dx),%es:(%edi)
 8048095:   6c                      insb   (%dx),%es:(%edi)
 8048096:   6f                      outsl  %ds:(%esi),(%dx)
 8048097:   20 77 6f                and    %dh,0x6f(%edi)
 804809a:   72 6c                   jb     8048108 <size+0x6a>
 804809c:   64 0a 0c 00             or     %fs:(%eax,%eax,1),%cl

0804809e <size>:
 804809e:   0c 00                   or     $0x0,%al
    ...

print関数のpushl %ebpmovl %esp, %ebpはespが刻々と変化するので,一時的に%ebpに退避させているだけです。

あとは、%espがどのような挙動になっているかを確認しましょう:

gdb
# 起動直後の$esp .. 0xbffff770
# print関数をcallした直後
(gdb) x/8xw $esp
# 0x08048063: `call   8048083 <exit>`へのアドレス(最終的にretでこのアドレスにjumpします)
# 0x0804809c: size(0x0c)へのアドレス
# 0x0804808f: messageへのアドレス
# その先はガラクタ
0xbffff764:     0x08048063      0x0804809c      0x0804808f      0x00000001
0xbffff774:     0xbffff898      0x00000000      0xbffff8be      0xbffff8cf

# print関数で`mov    %esp,%ebp`した直後の%esp
(gdb) x/8xw $esp
# 0x00000000: %ebpの値
# 0x08048063: `call   8048083 <exit>`へのアドレス(最終的にretでこのアドレスにjumpします)
# ...
0xbffff760:     0x00000000      0x08048063      0x0804809c      0x0804808f
0xbffff770:     0x00000001      0xbffff898      0x00000000      0xbffff8be

それで、movl 8(%ebp), %edxでやっているのは、sizeとかmessageの取り出しです:

%ebp メモリアドレス 中身
12(%ebp)-> 0xbffff76c 0x0804808f ->%ecx
8(%ebp)-> 0xbffff768 0x0804809c ->%edx
4(%ebp)-> 0xbffff764 0x08048063
-> 0xbffff760 0x00000000

みたいな風になり、あとはsys_writeシステムコールを呼んでいるだけです。最後にmain関数内のsubl $8, %espでstack pointerの場所を戻しています。

引数を渡す時の挙動については、https://vanya.jp.net/os/x86call/index.html も詳しいです。(ただし、このサイトではIntel 記法で書かれているので、srcとdstが逆であることに注意する)

linker script

linker scriptとは、メモリ割当を設定できるファイルです。linker scriptを通して、ロード時に展開する(virtual) addressが決定されます。
普段目にすることはないと思うのですが、gccのリンク時にも暗黙にlinker scriptが使われています。これはld --verboseで見れます。( https://gist.github.com/knknkn1162/48af994c2559a454f3caad03c2d1b34f においた )。リンカスクリプトはld -Tコマンドでも指定できます。

本シリーズでは、linker scriptを生のママで読むのは本当に必要な部分以外は避け、

  • objdumpコマンド(objdump -d ./a.out)でdisassembleした出力と

  • linker map(これは-ld Mで出力される(MはMapの意味))

の2種類を用意することでメモリ割当の様相を掴むことにします。

例えば、一番最初のhello worldのコード

hello0.S
.text
  .globl main

main:
  # sys_write(0x04)の呼び出し
  movl $0x04, %eax
  movl $0x01, %ebx
  movl $message, %ecx
  movl $12, %edx # length
  int $0x80
  # sys_exit(0x01)の呼び出し
  movl $0x01, %eax
  xorl %ebx, %ebx
  int $0x80

message:
  .ascii "Hello world\n"

だと、linker script

hello.ld
# これは、ldのdefaultのlinker scriptをかなり簡略化したものです。
SECTIONS
{
  /* location counter */
  . = 0x08048000 + SIZEOF_HEADERS;
  .text : { *(.text) }
  PROVIDE(etext = .);   /* Define the 'etext' symbol to this value */
  .rodata : { *(.rodata) }

    PROVIDE(data = .);
  .data : { *(.data) }
    /* Adjust the address for the data segment to the next page */
    /*. = ALIGN(0x1000); */
    PROVIDE(edata = .);
  .bss : { *(.bss) }
    PROVIDE(end = .);

    /DISCARD/ : {
        *(.eh_frame_hdr .note.gnu.build-id)
    }
}

をそのまま読むよりも、linker map

$ ld -T hello.ld hello0.o -M

Memory Configuration

Name             Origin             Length             Attributes
*default*        0x0000000000000000 0xffffffffffffffff

Linker script and memory map

                0x0000000008048054                . = (0x8048000 + SIZEOF_HEADERS)

.text           0x0000000008048054       0x2b
 *(.text)
 .text          0x0000000008048054       0x2b hello0.o
                0x0000000008048054                main
                0x000000000804807f                PROVIDE (etext, .)

.rodata
 *(.rodata)
                0x000000000804807f                PROVIDE (data, .)

.data           0x000000000804807f        0x0
 *(.data)
 .data          0x000000000804807f        0x0 hello0.o
                0x000000000804807f                PROVIDE (edata, .)

.bss            0x000000000804807f        0x0
 *(.bss)
 .bss           0x000000000804807f        0x0 hello0.o
                0x000000000804807f                PROVIDE (end, .)

/DISCARD/
 *(.eh_frame_hdr .note.gnu.build-id)
LOAD hello0.o
OUTPUT(a.out elf32-i386)

と実行ファイル(./a.out)をdisassembleしたもの

$ objdump -d ./a.out

./a.out:     file format elf32-i386


Disassembly of section .text:

08048054 <main>:
 8048054:   b8 04 00 00 00          mov    $0x4,%eax
 8048059:   bb 01 00 00 00          mov    $0x1,%ebx
 804805e:   b9 73 80 04 08          mov    $0x8048073,%ecx
 8048063:   ba 0c 00 00 00          mov    $0xc,%edx
 8048068:   cd 80                   int    $0x80
 804806a:   b8 01 00 00 00          mov    $0x1,%eax
 804806f:   31 db                   xor    %ebx,%ebx
 8048071:   cd 80                   int    $0x80

08048073 <message>:
 8048073:   48                      dec    %eax
 8048074:   65                      gs
 8048075:   6c                      insb   (%dx),%es:(%edi)
 8048076:   6c                      insb   (%dx),%es:(%edi)
 8048077:   6f                      outsl  %ds:(%esi),(%dx)
 8048078:   20 77 6f                and    %dh,0x6f(%edi)
 804807b:   72 6c                   jb     80480e9 <message+0x76>
 804807d:   64                      fs
 804807e:   0a                      .byte 0xa

を見た方がメモリアドレスの配置の雰囲気はなんとなくつかめるのではないでしょうか?

これと合わせて、「C言語、アセンブリ(gnu assembly)もろもろの知識」で紹介したリンク

ELF(Executable and Linking Format) に関しては、http://www.cirosantilli.com/elf-hello-world/ および、https://qiita.com/knknkn1162/items/c67ae7c2ef71a713adf8 を読みましょう。手を動かして確かめるのを推奨します。用語集としてbinary hacksパラパラ見るのもお勧めです。

も一緒に見ると、ELF, assembly, linker scriptの関係性が見えてくるかと思います。

boot処理のコードを読む際もこの節で紹介した手法を用いて読んでいきたいと思います~

補足

  • xv6のmemory配置(0x00000000 ~ 0x7fffffffまで)

スクリーンショット 2018-09-20 11.44.54.png
https://pdos.csail.mit.edu/6.828/2018/xv6/book-rev11.pdf のFigure2.2

0x80000000 ~ 0xffffffffまではkernel内部で扱うmemory領域になります。


  1. bochsとはx86のエミュレータで外部とのやり取りをemulateしてくれるツールです。qemuに内蔵されています。リンク先見ると、Do NOT consider this information as complete and accurate.みたいな風に書いてあるのですが、I/O port一覧としていろんなところで引用されており、信頼度高いと個人的に感じるので、レファレンスとして使っていきたいと思います。 

  2. 自分はnasmから入って、x86_64 Linux Assemblyシリーズ(youtube)を推しているのですが、GNU aseembly入門で良いサイトなどがあれば教えてください:ok_hand: 

  3. xv6のメモリ配置はstackの位置がlinux x86のそれとは若干異なりますので、注意してください。補足に図だけ載せてます。本記事では、あくまでlinuxでのメモリ配置について述べています。 

69
51
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?