pythonインストールしてLinuxカーネルのドキュメントを読む

Linuxカーネル (linux-4.17.1.tar.xz) をダウンロードした。 最近のは、

 

$  make  htmldocs

 

と打ち込めばHTML形式のドキュメントを生成できるらしい。

 

しかし、その際、sphinx-buildというコマンドが必要なのだそうな。 これは、どうやら Python のパッケージ (PyPI) のやつらしい。 VineLinux(6.5)にもあるようだが、古いものだったので、Python2.7からインストールすることに。

 

Python公式 (https://www.python.org/)から、Python-2.7.15をダウンロードし、いつも通り(./configure; make; make install)ビルドするが、これだと pip っていうコマンドがインストールされなかったので、

./configure  --with-ensurepip=install

make

make install

 という手順で pip も入った。

 

そして、sphinxのインストール。

$  pip  install  -U  sphinx

 

chrootコマンドを使う

chrootコマンドは、指定のディレクトリを、ルートディレクトリ(/)に変更するというコマンドです。

 

そのディレクトリを起点 (/) にしてしまったわけですから、元の /bin のコマンド群や /usr/lib のライブラリにはアクセスできません。 「chroot後、シェルを起動してくれ」といっても、新しい仮想的な環境の中に /bin/sh が存在しなければシェルを起動できませんし、プログラムが必要とする実行時ライブラリも入っていなければ起動不可能です。

 

しかし、新しいシステム環境を一から構築し、インストール、実行する場合には便利な機能です。

 

chrootで、起点を変更したあと、何かできなければ意味がありません。 デフォルトではシェルが起動するようになっているようですが、任意のコマンド実行することもできます。 chroot コマンドの形式:

 

chroot  directory  [command  [args] ...]

 

なので、空のディレクトリを作っただけで chroot してもエラーになってしまいます。 また、そのコマンドが必要とする実行時ライブラリ(共有ライブラリ)もなければなりません。 しかし、メモリ効率は無視してでも、単に実行するだけなら、静的リンクでビルドすれば、共有ライブラリ不要で実行可能です。

 

プログラムを静的リンクでビルドするには、gcc コマンドに -static オプションを指定します。

 

gcc  -static  -o program  source.c

 

しかし、glibcの静的ライブラリがシステムにインストールされてないかもしれません(私の場合はそうでした)。 その場合はパッケージマネージャから、glibcライブラリのスタティック版をインストールする必要があります。 VineLinuxでは、以下のようにインストールできました。

 

$  sudo  apt-get  install  glibc-static

 

このようにビルドされたプログラムを適当なディレクトリに配置すれば、chrootで実行可能です。 あと、chrootはスーパーユーザ権限が必要です。

 

$  sudo  chroot  MyDir/  /program

 

これでプログラムは実行されるでしょう。

 

もし、これに物足りなさを感じたら、ファイルシステムのすべてのファイルをコピーするという荒技もあります!

 

自作 init プロセスをパワーアップ?

昨日の「initをぶっ壊そうぜ! - ただの日記」続き。

 

コマンドライン引数と環境変数

Linuxで最初に実行されるプログラム init はどうなっているのか? そのために、昨日は init を適当な hello,world! などで置き換えるということをしました。

 

init は特別なプロセスですが、コマンドライン引数と環境変数もきちんとあります。 以下のようなプログラムを作って実行させてみます。

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/reboot.h>

 

extern char **environ;

 

int main(int argc, char *argv[ ])
{
     char buf[256], cmd;
     int i;

 

    // コマンドライン引数を表示する
    printf("\n\n");
    for (i=0; i<argc; i++)
        printf("argv[%d]: %s\n", i, argv[i]);

 

    // 環境変数を表示する
    printf("\n\n");
    i = 0;
    while (environ[i] != NULL) {
        printf("%s\n", environ[i]);
        i++;
    }

 

    // 入力待ち
    cmd = '\0';
    memset(buf, 0, sizeof(buf));
    printf("\n\n command >> ");
    fgets(buf, sizeof(buf), stdin);
    sscanf(buf, "%c", &cmd);

 

    switch (cmd) {
    case 'h':
        reboot(0x4321fedc); // シャットダウン
        break;
    default:
        reboot(RB_AUTOBOOT); // 再起動
        break;
    }

 

    printf("reboot() error!\n");

 

    return 0;
}

 

このプログラムは init として実行されたときのコマンドライン引数と環境変数をダンプし、その後、入力待ちしてシステムをシャットダウンか再起動します。

 

実行結果

 どうやら init のコマンドライン引数はカーネルパラメータとして与えたものがそのまま入っているようです。 環境変数もいくらか設定されています。

 

自作 init から本来の init を起動する

execv()関数で本来の init に成りすましましょう。

argv[0] には、本来の init のパスを設定しておきます。

以下は、main()関数の中のコードです。

 

argv[0] = "/sbin/init";
execv("/sbin/init", argv); 

 実行結果

簡単なコードですが、これで、本来の通常起動に戻るようです。

 

自作 init からシェルを起動する

デーモンの起動などカーネル以外のシステム初期化を行なっていない段階ですが、シェル (/bin/sh) も一応、起動できるようです。 (しかし、サービスが起動してないことから、何らかの制限が潜んでいるかもしれません。)

 

自作 init のプロセスは維持しておいた方がいいかもしれないので、ここでは fork して /bin/sh に exec します。

 

pid_t p;

 

...

 

p = fork();
if (p == -1) {
    printf("fork() error.\n");
} else if (p == 0) {
    execl("/bin/sh", "/bin/sh", NULL);
} else {
    if (waitpid(p, NULL, 0) == -1)
        printf("waitpid() error.\n");
}

 

シェルで exit コマンドを実行してシェルを抜けると、元の自作 init の処理に戻ります。

 

 まとめ

ここまでの機能を1つにまとめたプログラムを作りました。 メニューから処理を選んで実行するプログラムです。

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reboot.h>

 

extern char **environ;

 

int main(int argc, char *argv[ ])
{
    char buf[256];
    char cmd;
    pid_t p;
    int i;


    while (1) {
        printf("\n");
        printf("i) start /sbin/init\n");
        printf("s) run /bin/sh\n");
        printf("a) show argv\n");
        printf("e) show environ\n");
        printf("h) halt\n");
        printf("r) reboot\n");
        printf(" >> ");

 

        cmd = '\0';
        memset(buf, 0, sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        sscanf(buf, "%c", &cmd);

 

        switch (cmd) {
        case 'i':
            argv[0] = "/sbin/init";
            execv("/sbin/init", argv);
            break;
        case 's':
            p = fork();
            if (p == -1) {
                printf("fork() error.\n");
            } else if (p == 0) {
                execl("/bin/sh", "/bin/sh", NULL);
            } else {
                if (waitpid(p, NULL, 0) == -1)
                    printf("waitpid() error.\n");
            }
            break;
        case 'a':
            for (i=0; i<argc; i++)
                printf("argv[%d]: %s\n", i, argv[i]);
            break;
        case 'e':
            i = 0;
            while (environ[i] != NULL) {
                printf("%s\n", environ[i]);
                i++;
            }
            break;
        case 'h':
            if (reboot(0x4321fedc) == -1)
                printf("reboot() error.\n");
            break;
        case 'r':
            if (reboot(RB_AUTOBOOT) == -1)
                printf("reboot() error.\n");
            break;
        }
    }

 

    return 0;
}

 

initをぶっ壊そうぜ!

※以下の内容はOSを起動不可にする危険性のある操作を行います。 実験台PC、もしくはエミュレータなどの使い捨て環境でお試しください。

 

 Linuxオープンソースなんだけど、相変わらずその中身はよく分からないことが多い。 ソース読めとか、デバッガ使えとかプログラムを解析する方法はいろいろとあるようだが、結局、知識がないと分からないものでもあったりする。

 

今回のターゲットは、起動プロセスの1つ init だ。 init とは /sbin/init のこと。 これは、カーネルが起動した直後、最初に実行されるプログラムである。 こいつをどうするのか? 簡単なプログラム、たとえば、「Hello,world!」で置き換えよう! そう、Hello,world! なんかで置き換えてしまうのだから、もうOSは起動できないぜ! ただ、これをやるくらいなら特に手順を示さなくてもできるだろう。 C言語でプログラム書いて、rootになって /sbin/init を置き換えればいいだけだ。

 

しかし、今後、init をいじくりまわすことになると、プログラム一回、実行するたびにOSを再インストールするというのは非常に面倒くさい。 せめて、もっと便利にならないか?

 

カーネル起動時に実行する init を指定する

実は、Linuxカーネル、最初のプロセスとして /sbin/init 以外を実行するように指定することもできる。 ところで、カーネルの起動パラメータってどうやって指定するのか? それは、通常、ブートローダの仕事だろう。 つまり、GRUBの起動メニューに、「通常起動」のほかに「自分で作った init で起動」という項目を追加すればよいのだ。

 

GRUB は、古いバージョンの GRUB Legacy と GRUB2 があるが、Vine Linux 6.5 では、まだ古いほうの GRUB だと思うので、以下の方法は Ubuntu などには当てはまらないかもしれない。

 

Vine Linux では、/boot/grub/menu.lst が GRUB のOS起動メニュー画面の設定ファイルだ。 これを編集して、カーネルパラメータを書き換える。

 

/boot/grub/menu.lst の内容

# menu.lst generated by anaconda
#
# Note that you do not have to rerun grub after making changes to this file
# NOTICE:  You have a /boot partition.  This means that
#          all kernel and initrd paths are relative to /boot/, eg.
#          root (hd0,0)
#          kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
#          initrd /initrd-version.img
#boot=/dev/sda
default=0
timeout=5

title Vine Linux (Current kernel)
	root (hd0,0)
	kernel /vmlinuz ro root=/dev/VolGroup00/LogVol00 resume=swap:/dev/VolGroup00/LogVol01 vga=0x314
	initrd /initrd.img

title Vine Linux (Previous kernel)
	root (hd0,0)
	kernel /vmlinuz.old ro root=/dev/VolGroup00/LogVol00 resume=swap:/dev/VolGroup00/LogVol01 vga=0x314
	initrd /initrd.old.img

 

簡単に説明すると、title で始まっている行がメニュー項目の文字列で、それ以下に起動コマンドが続く。 kernel というコマンドが起動するカーネルを指定する行になっている。 /vmlinuz というのがカーネルの本体のファイルらしい。 そのあとの文字列がカーネルの起動パラメータになっている。 他のパラメータに、どんな意味があるのかはわからないので、とりあえず、メニュー項目の1つをコピーして、それを書き換えることにする。

 

最初のプログラム init を指定するには、パラメータに 'init=' というものを追加すればいい。 もし、/mybin/init という場所に自作の init があるとすれば、以下のようになる。 (※黄色の部分が変更・追加したところ)

title myinit
	root (hd0,0)
	kernel /vmlinuz init=/mybin/init ro root=/dev/VolGroup00/LogVol00 resume=swap:/dev/VolGroup00/LogVol01 vga=0x314
	initrd /initrd.img

上記のコマンド群をファイルの一番最後にでも追加すれば、自作 init をメニュー画面で選択してシステムを起動できるようになるはず。

 

なお、GRUBドキュメンテーションGNU GRUBのホームページ(

https://www.gnu.org/software/grub/grub-documentation.html

)に、カーネルパラメータについては、カーネルソースツリー(パッケージをダウンロードする、もしくは、/usr/srcを探す)の Documentation/kernel-parameters.txt に書いてある。

 

 シャットダウンと再起動

自作の init が Hello,world! を表示したあと、どうなるのか? 通常であれば、init はシステムのサービスを起動して待機するはずなのだが、Hello,world! の場合だと、もう何もやることがない。 もし、ここで一般のプログラムのように普通に終了してしまうと、カーネルは「カーネルパニック」というエラーを引き起こして止まってしまう。 この状態で電源を強制終了しても、いまどきのPCなら特に問題にならないだろうが、それはあくまで非常用の手段だから、あまり多用したくはない。 もっとスマートにシャットダウン、もしくは再起動を行うにはCライブラリ関数の reboot() を使用する。 「man 2 reboot」 コマンドでマニュアルを調べてみよう。

 

reboot()を実行するにはスーパーユーザ権限が必要になるが、init はちょうどスーパーユーザ権限で動くプログラムなので実行可能だ。

 

reboot() の形式は次のとおり。

#include <unistd.h>

#include <sys/reboot.h>

 

int reboot(int cmd);

 マニュアルの方は、ちょっと注意して読まないといけない。 システムコールとライブラリ関数の形式が異なっているようだ。 でも、それ以外は簡単な関数なので、一度、理解すれば問題ないだろう。

 

PCの再起動は次のコード。

reboot(RB_AUTOBOOT);

 

PCのシャットダウンは次のコード。

reboot(0x4321fedc);

 (※定数名ではなく値です)

---- ■追記 ----

シャットダウンの定数名は RB_POWER_OFF

って書けばいいらしい。 sysvinit-2.88dsf のソース見て知った。

sys/reboot.h の中身は見てなかった。。。

sysvinit: http://savannah.nongnu.org/projects/sysvinit

----------------

 

 Hello,world! を表示した後、再起動を行うプログラムは以下のとおり。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/reboot.h>

 

int main(int argc, char *argv[])
{
    printf("\n\nHello,world!\n\n");

 

    sleep(20);

 

    reboot(RB_AUTOBOOT);

 

    // 到達しない

    return 0;
}

 

 

この他、fgets()など標準入力から入力もできるようなので、独自コマンドを入力してシャットダウンを指示するようにするのもいいかもしれない。

 

pkg-configでライブラリのバージョンを調べる

--modversionオプションを使う。

 

$ pkg-config --modversion libxml-2.0

$ pkg-config --modversion gtk+-2.0

 

など。

 

argp

Linuxとかのプログラムでコマンドライン引数を解析するための方法として、argpというインターフェースがあります。 glibc (GNU C Library) の中にあります。

 

コマンドライン引数解析は、getopt()およびgetopt_long()を使っても解析できます。 むしろ、こっちのほうが使い方は簡単なような気がします。

 

しかし、getopt()などよりも argp のほうが(マニュアルによると?)色々できてメンテが楽らしいので、これから argp を試してみたいと思います。

 

最初のプログラム

これで一応動くらしい。

 

#include <stdlib.h>
#include <argp.h>

 

int main(int argc, char *argv)
{

    argp_parse(NULL, argc, argv, 0, NULL, NULL);
    exit(0);
}

 

 

このプログラムは何もしませんが、コマンドラインから --help オプションは機能します。 以下のように実行するとヘルプメッセージが表示されるはずです。

 

$  ./test1  --help

 

一般的なコマンドの cat とか ls でも --help を付けて実行するとヘルプメッセージが表示されますが、それらと同じものが argp ではすでに内蔵されているのです。

 

--version オプション(バージョン情報を表示するオプション)も使えるようにするには、グローバル変数 argp_program_version を定義して、表示する文字列を代入するだけです。 あと、--help オプションのヘルプメッセージで、バグ報告用のアドレスを付け加えるには、同様にグローバル変数 argp_program_bug_address を定義し文字列を代入するだけです。 これらを設定したプログラムは以下のようになります。

 

#include <stdlib.h>
#include <argp.h>

 

const char *argp_program_version = "Ver1.0.0";
const char *argp_program_bug_address = "foo@bar.baz";

 

int main(int argc, char *argv)
{
     argp_parse(NULL, argc, argv, 0, NULL, NULL);
     exit(0);
}

 

 

 なんで、グローバル変数宣言しただけで機能してるのかは知りませんが、これで良いそうです(たぶん、weak symbol ってやつかな?)。

 

C言語プログラムからアセンブリ言語コードを得る

※ここでは、Linuxgcci386 CPUを使っています。

 

プログラムは、C言語アセンブリ言語機械語という過程を経て実行に至るはずですが、現在はコンパイラが一括して処理してしまうので、この過程を直接見ることができません。 より人間側に近い高級言語から、より機械に近いアセンブリ言語マシン語になっていくつながりを見ることができないのです。

 

しかし、gccの -S オプションを使うと、

簡単にC言語のソースのアセンブリを出力できます。

gcc -S test1.c

適当に、Hello,world!でもコンパイルしてみましょう。

#include <stdio.h>

 

int main()

{

    printf("Hello,world!\n");

    return 0;

}

これを、gccで -S を使いアセンブルすると test1.s のようなアセンブリ言語のソースファイルが出力されます。

アセンブリは得られたけど、これもソースだから、このままだと実行できません。 実は、gccアセンブリソースもコンパイルできます。 ファイル名の拡張子が .s になっているとアセンブリソースと認識されます。

gcc test1.s

これで、出力の実行可能ファイル (a.out)を得られます。

アセンブリソースからオブジェクトファイルで出力することもできます。

gcc -c test1.s

asコマンド (gas) でアセンブルすることも可能です

$  as  -otest1.o  test1.s