ラベル UNIXプログラミング の投稿を表示しています。 すべての投稿を表示
ラベル UNIXプログラミング の投稿を表示しています。 すべての投稿を表示

GNU/Linuxをソースコードからビルドする手順

私たちが普段、Linuxと読んでいるものの実態は実は、Linux kernel + GNU softwareの複合環境なのです。図で示しますと下のようになります。

したがって、GNUの人々はこのような環境のことをGNU/Linuxと呼ぶように主張しています。確かに一理あると思います。

ところで、GNU/Linuxの環境をソースコードよりビルドする時は、段階が多くとても混乱してしまいます。
そこで、今回は各段階の説明をしてみたいと思います。



#1.ファイルシステムの作成とデバイスファイルの作成
  • Filesystem Hierarchy Standard(FHS)に基づいたファイルシステムを構築する。
  • MAKEDEVというスクリプトを用いて/dev内にデバイスファイルを生成する。
#2.各ソフトウェのコンパイルと導入
  • 基本は./configure, make, make installを繰り返すだけだが、./configureの時にオプションを大量に指定したり、make installの時に、インストール先のディレクトリを指定する必要がある。
  • まずは、glibcをコンパイル。glibcは各種コマンドやソフトウェアを実行する際に必ず必要。GNU/Linux環境はほとんどのコマンドやソフトウェアでダイナミック・リンクにてglibcを利用するため、このライブラリがないと各種コマンドが実行できない。
  • ncurses : コンソール端末の表現力を向上させるライブラリ。bashの動作に必須。
  • bash : Linuxの標準シェル
  • util-linux : ログイン関連やttyコマンド、ディスク操作
  • coreutils : シェルの中でよく使うコマンド群を集めたソフトウェアパッケージ。cat, ls, suといったシェル関連コマンドを含む。
  • etc.
#3.カーネルのコンパイル
  • 設定項目が膨大なのので、開発環境の構成ファイルを流用すると良い。
  • makeは場合によって一晩かかる時がある。
  • -jのオプションで時間の短縮が可能。
#4.initramfs(initrd)ファイルの作成
  • initramfsとは、カーネルをロードした後で、一時的にメモリー上に展開したルートファイルシステムを、マウントできるように準備するためのファイルである。
  • 少し前までinitrdという名称で呼ばれていたが、現在はinitramfsに変更されている。
#5.ルートファイルシステムの完成
  • ブート時に起動するプログラムやユーザ名、サービス名、パスワードといった最低限必要となるファイルを作成する。
  • ex)inittab, rc.sysinit, rc*, passwd, group, profile, rsyslog.conf, services, fstab, hosts
#6.記憶媒体に書き込む
  • fdiskによりパーティションを作成
  • mke2fsなどでパーティションをフォーマット
  • ビルドしたGNU/Linuxシステムをシュリンクしてから、アーカイブ、記憶媒体にコピー。
  • ブートローダー(LILO, GRUB)を導入
  • grub.confの設定
#7.各種アプリケーションを導入する
  • 必要に応じて、vimなどのテキスト編集コマンドや、Apacheなどのサーバー関連アプリケーションを追加導入する。
【参考文献】
『日経Linux 2011年11月号』日経BP社

『日経Linux 自分で作るLinux OS 2006』日経BP社
『Software Design 総集編 2001 - 2012』技術評論社







buildrootを用いてusbブートのLinuxをビルドする。

ネット上の様々な有用情報を頼りにして、buildrootとLinuxカーネルのソースコードから、usbブート可能なLinuxをビルドし、usbにgrubとともにインストールすることができました。一連の行程を備忘録として残しておきます。

USBから起動できる最小構成のLinuxをビルド。

<参考Web site>

<(0)開発環境>
Vine Linux 5.1 on Let's note CF-T7
ユーザー名:kappa

※今回作成するMinuxというネーミングはhttp://plaza.rakuten.co.jp/slothstyle/diary/201105150000/のブログのものです。すばらしい内容で大部分を参考にさせていただきましたので、敬意を込めてそのままの名前を使用させていただきます。

<(1)ルートファイルシステムの作成>
Buildrootというソフトウェアを利用/buildrootとはルートファイルシステムを作成するためのtoolchan。uClibcやbusyboxをはじめとする様々なプログラムをビルドするためのMakefileやpatchで構成されている。
###Buildroot(buildroot-2011.02.tar.gz)をダウンロードする。
$ pwd
/home/kappa
$ mkdir minux
$ cd ./minux
$ wget http://buildroot.uclibc.org/downloads/buildroot-2011.02.tar.gz
$ tar -zxvf buildroot-2011.02.tar.gz

###Buildrootのソースコードダウンロード先ディレクトリを作成。Buildrootの設定でダウンロード先を指定することで、ダウンロードファイルを一箇所にまとめて共通化が可能。
$ pwd
/home/kappa/minux
$ mkdir buildroot-dl

###ビルドディレクトリを作成。 "minux" を親ディレクトリとし、Buildroot用に "br" という子ディレクトリを作成。
$ pwd
/home/kappa/minux
$ mkdir br

###"allnoconfig" でBuildrootの設定を初期化します。
$ pwd
/home/kappa/minux
$ make -C  /home/kappa/buildroot-2011.02 O=/home/kappa/br allnoconfig

###Buildrootの設定
$ cd ./br
$ make menuconfig

以下のようにオプションを設定。

(a)
Build options -> Download dirの
"$(TOPDIR)/dl"
"/home/admin/minux/buildroot-dl"
に変更。

(b)
BusyBoxをビルドするように設定。
Package Selection for the target -> BusyBox /// ON

(c)
ルートファイルシステムのイメージファイルをcpio形式に指定。
Target filesystem options -> cpio root filesystem /// ON

exitした後、ビルドする。
(1時間ほどかかりました。自分の環境 core 2 duo, 1.20GHz。ファイルダウンロードが行われるため、インターネット接続は必須です。)

$ make all

### fakeroot_1.9.5..tar.gzがダウンロードできないことが理由でインストールが止まるので、該当ファイルをダウンロードし適切な位置に配置
$ mv fakeroot_1.9.5.tar.gz /home/kappa/minux/buildroot-dl/
$ make all

ビルドに成功すると、/br/images/rootfs.cpio" というファイルが作成されます。これがcpio形式でアーカイブされたルートファイルシステムのイメージファイルになる。
###ルートファイルシステム(rootfs.cpio)を圧縮し、/home/kappa/minuxにinitrdという名前でコピーしておく。
$ gzip /home/kappa/minux/br/images/rootfs.cpio
$ cp /home/kappa/minux/br/images/rootfs.cpio.gz /home/kappa/minux/initrd

<(2)最小構成でkernelをビルド>
続いてUSBブートするための最小構成のカーネルをビルドする。
###linux-2.6.37.2のソースコードをダウンロードしてビルドした。
 $ pwd
/home/kappa/minux/
$ wget http://www.kernel.org/pub/linux/kernel/v2.6//linux-2.6.37.2.tar.bz2
$ tar jxf linux-2.6.37.2.tar.bz2
$ mkdir kernel

###allnoconfigで設定を初期化を行う。
$ make -C ./linux-2.6.37.2 O=/home/kappa/minux/kernel allnoconfig
$ cd kernel
$ make menuconfig

###以下のオプションを有効化する。モジュール化はせず、カーネルに組み込む。(モジュールはallnoconfigで無効になっている。)

・General setup -> Initial RAM filesystem and RAM disk (initramfs/initrd) support /// ON
!!圧縮ファイルの展開関連のオプションも自動的に有効化される。!!

・Executable file formats / Emulations
-> Kernel support for ELF binaries /// ON
-> Write ELF core dumps with partial segments /// ON (auto)

###ビルドします。"bzImage"が生成されれば成功。

$ make bzImage
...
Setup is 14008 bytes (padded to 14336 bytes).
System is 770 kB
CRC 5b2917a6
Kernel: arch/x86/boot/bzImage is ready (#1)
のように表示が出れば成功。




<(3)USBメモリ(/dev/sdb)へのMinimal Linux の導入>

###まずはusbをフォーマット
$ su 
# fdisk /dev/sdb
p (パーティション内容の表示)
d (パーティションの削除)
n (パーティションの作成. デフォルトで/dev/sdb1というパーティションが生成する。)

###usbの/dev/sdb1をext3でフォーマット
# mkfs.ext3 /dev/sdb1

###usbをアマウントして再度場所を変えて(/mnt/usb)マウント(わかりやすくするためである)
# umont /dev/sdb1
# mount /dev/sdb1 /mnt/usb


###grubをインストールする。
# mkdir /mnt/usb/grub
# cp /boot/grub/stage* /boot/grub/e2fs_stage1_5 /mnt/usb/grub/
# umount /mnt/usb

###/dev/sdb1にMBR(master boot record)をインストールする。MBRはブート処理専用としてデバイス上に各区帆された512byteの非表示領域である。MBRのインストールには、GRUB関連の処理を行うシンプルなコマンドシェル、GRUBシェルを使用する。
# grub
grub> device (hd0) /dev/sdb
grub> root (hd0, 0)
grub> setup (hd0)
grub> quit

###usbの/dev/sdb1にビルドしたルートシステム(/home/kappa/minux/initrd)とカーネルイメージ(/home/kappa/minux/kernel/arch/i386/boot/bzImage)を/dev/sdb1のルート(/mnt/usbにマウントした状態では、ビルド環境のマシーンから見ると/mnt/usb/がルートディレクトリ)にコピーしていく。
# mount /dev/sdb1 on /mnt/usb
# cp /home/kappa/minux/initrd /mnt/usb/
# cp /home/kappa/minux/kernel/arch/i386/boot/bzImage /mnt/usb/


###grubの設定ファイルに、/dev/sdb1の構成を書き込む。 bzImage及び、initrdが/dev/sdb1のルートディレクトリにおいてあるだけのので、素直にその旨を記述する。
# vim /boot/grub/menu.lst

////////////////////////////////////
title Minimal Linux
kernel /bzImage root=/dev/sdb
initrd /initrd
////////////////////////////////////

<(4)おわり>
再起起動すれば、BIOSの設定がUSB起動になっていれば、CUIでLinuxが起動する。
"root" ユーザーでログイン可能。
パスワードは未設定である。

アドレス空間の旅


メモリ確保の方法によって確保される領域の番地に差異が生じるかを検討しました。

// array_address.c
#include<stdio.h> // printf
#include<stdlib.h> // malloc, free
#define LENGTH 3
int main(void)
{

//配列の宣言と同時に、特定の大きさだけ仮想アドレス空間内に確保している。
char array_static_1[LENGTH];
char array_static_2[LENGTH];
char *array_dynamic_ptr_1;
char *array_dynamic_ptr_2;
int i;

/*mallocにより(int型の大きさ)× LENGTHだけの値を与える。
戻り値は、mallcにより確保された領域の先頭のアドレスである/。
*/
array_dynamic_ptr_1 = (char *) malloc (sizeof (char) * LENGTH);
array_dynamic_ptr_2 = (char *) malloc (sizeof (char) * LENGTH);


for(i = 0; i<LENGTH; i++)
printf("array_static_1[%d]=%p\n", i, &array_static_1[i]);
for(i = 0; i<LENGTH; i++)
printf("array_static_2[%d]=%p\n", i, &array_static_2[i]);
for(i = 0; i<LENGTH; i++)
printf("array_dynamic_ptr_1[%d]=%p\n", i, &array_dynamic_ptr_1[i]);
for(i = 0; i<LENGTH; i++)
printf("array_dynamic_ptr_2[%d]=%p\n", i, &array_dynamic_ptr_2[i]);
//確保したメモリ領域を開放。
free(array_dynamic_ptr_1);
free(array_dynamic_ptr_2);

return 0;
}




$ gcc -o array_address array_address.c

$ ./array_address
array_static_1[0]=0xbf8b592d
array_static_1[1]=0xbf8b592e
array_static_1[2]=0xbf8b592f
array_static_2[0]=0xbf8b592a
array_static_2[1]=0xbf8b592b
array_static_2[2]=0xbf8b592c
array_dynamic_ptr_1[0]=0x80dd008
array_dynamic_ptr_1[1]=0x80dd009
array_dynamic_ptr_1[2]=0x80dd00a
array_dynamic_ptr_2[0]=0x80dd018
array_dynamic_ptr_2[1]=0x80dd019
array_dynamic_ptr_2[2]=0x80dd01a


この結果から大変興味深いことがわかります。
それは、mallocで確保される領域の方が、静的に割り当てられる領域の方がアドレス番地が若いことがわかります。そして、同じカテゴリの変数は、連続して割り当てられていることがわかります。

このことを理解するには、仮想アドレス空間の概念を理解する必要があるようです。

UNIXの場合、プログラムが実行されると、各プロセスごとに、
32bitOSの場合、0x00000000-0xffffffff番地までの連続した合計32bit(=4GB)のアドレス空間が割り当てられるとのことです。

もちろん、このアドレス空間というのは仮想メモリのことであり、実メモリとはまったく異なります。OSがリソース管理のために提供するサービスだと考えればよいです。

そして、mallcにより確保される領域は、データ領域とよばれる所であり、それは比較的若い番地が割り当てられています。
一方、静的に確保された領域は、スッタク領域と呼ばれ、比較的大きい番地に割り当てられています。スタック領域は、大きい番地から小さい番地に向けて変数が確保されていくのが特徴的です。

連続するアドレスをいかに確保するか

配列を扱うために、仮想アドレス空間に特定の領域を確保するときは、一般に配列を宣言するときに同時に静的に確保する方法と、mallcを用いて動的に確保し、freeで開放する方法の二つが存在します。

今回は、これら二つの方法を用いたプログラムを検討しました。


// array_static.c
#include<stdio.h> // printf
#define LENGTH 10
int main(void)
{

//配列の宣言と同時に、特定の大きさだけ仮想アドレス空間内に確保している。
int array[LENGTH];
int i;

for(i =0; i<LENGTH; i++)
{
array[i] = i * 3;
}

for(i = 0; i<LENGTH; i++)
{
printf("array[%d]=%d\n", i, array[i]);
}
}





// array_dynamic.c
#include<stdio.h> // printf
#include<stdlib.h> // malloc
#define LENGTH 10

int main(void){
int *array;
int i;

/*mallocにより(int型の大きさ)× LENGTHだけの値を与える。
戻り値は、mallcにより確保された領域の先頭のアドレスである。
*/
array = (int *) malloc (sizeof (int) * LENGTH);

for(i = 0; i < LENGTH; i++)
{
array[i] = i * 3;
}
for(i = 0; i < LENGTH; i++)
{
printf("array[%d]=%d\n", i, array[i]);
}
//確保したメモリ領域を開放。
free(array);

return 0;
}

低水準入出力と高水準入出力の速度の違い

低水準入出力関数(システムコールのwrite, read)は、呼び出しがとても遅いのでprintfなどの高水準のブッファリングを取り入れた標準関数を使用することが一般的に推奨されています。

今回は、低水準、高水準の両者で同様の処理をしたときにどの程度、所要時間に差が出るのかを検証するプログラムを勉強しました。

下に、二つのプログラムをのせますが、どちらも二つのファイルをオープンして一方から内容をバイト単位で読み込み、他方へバイト単位で書き出すプログラムです。

入出力処理の前後で、現在時刻を取得し、その差分で所要時間を計算させています。


○システムコールを使うタイプ



// high_level_io.c
#include<stdio.h> //fprintf
#include<stdlib.h>//exit
#include<sys/time.h> //gettimeofday
#include<sys/types.h>//read
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>


double time_pass()
{
struct timeval tv;
//現在の時刻を取得
gettimeofday(&tv, NULL);
//秒数に換算して値を返却
return tv.tv_sec + (double) tv.tv_usec * 1e-6;
}

int main(int argc, char *argv[])
{
int *fdin, *fdout;
double start_time, end_time;
char c;

if(argc != 3)
{
fprintf(stderr, "Usage: copy_highio <file1> <file2>\n");
exit(EXIT_FAILURE);
}

//入力ファイルを開く
if((fdin = open(argv[1], O_RDONLY)) < 0)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//出力ファイルを開く
if((fdout = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0)
{
perror(argv[2]);
exit(EXIT_FAILURE);
}

//開始時刻を取得
start_time = time_pass();

//ファイルよりバイト単位でデータを読み込んで、出力ファイルへ出力
while(read(fdin, &c, 1) == 1)
{
write(fdout, &c, 1);
}

//終了時刻を取得
end_time = time_pass();

//ファイルを閉じる。
close(fdin);
close(fdout);

printf("%.4f\n", end_time - start_time);
}





○標準入出力を使うタイプ


// high_level_io.c
#include<stdio.h> //fprintf
#include<stdlib.h>//exit
#include<sys/time.h> //gettimeofday

double time_pass()
{
struct timeval tv;
//現在の時刻を取得
gettimeofday(&tv, NULL);
//秒数に換算して値を返却
return tv.tv_sec + (double) tv.tv_usec * 1e-6;
}

int main(int argc, char *argv[])
{
FILE *fpin, *fpout;
double start_time, end_time;
char c;

if(argc != 3)
{
fprintf(stderr, "Usage: copy_highio <file1> <file2>\n");
exit(EXIT_FAILURE);
}

//入力ファイルを開く
if((fpin = fopen(argv[1], "rb")) == NULL)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//出力ファイルを開く
if((fpout = fopen(argv[2], "wb")) == NULL)
{
perror(argv[2]);
exit(EXIT_FAILURE);
}

//開始時刻を取得
start_time = time_pass();

//ファイルよりバイト単位でデータを読み込んで、出力ファイルへ出力
while(fread(&c, 1, 1, fpin) == 1)
{
fwrite(&c, 1, 1,fpout);
}

//終了時刻を取得
end_time = time_pass();

//ファイルを閉じる。
fclose(fpin);
fclose(fpout);

printf("%.4f\n", end_time - start_time);
}

#プログラムのビルド
$ gcc -o low_level_io  low_level_io.c

$ gcc -o high_level_io  high_level_io.c



#GNUが配布しているlsコマンドのソースコードをコピーしてみます


$ ./low_level_io ls.c  out.2
0.8041

$ ./high_level_io ls.c  out.1
0.0282


0.8041秒と、0.0282秒とは。。。。

ブッファリングを導入したことで飛躍的に処理が早くなっていることが一目両全です。

システムコールはやはり特別なものなんですね。





バイナリファイルの扱い

バイナリファイルの扱いについて勉強しました。


テキストファイルで保存する場合、 データをすべてテキストに変換する必要が出るため処理に時間がかかったり、サイズが大きくなってしまうというデメリットがあります。

そこで、メモリ中のデータをテキストに置き換えずにそのまま書き出すバイナリ形式のファイルが重宝されるようです。

プログラムを書く上でのポイントは、fopenをb(binary)モードで行うことです。

バイナリファイル中のイメージは、文字列に変換されていない”文字列”がバイナリ形式で並んでいる感じです。


// write_binary.c
#include<stdio.h> //perror
#include<stdlib.h> //exit

int main(void)
{
FILE *fp;
char string[] = "We are the world.";
char string2[] = "penguin";

//ファイルをwb(書き込み、バイナリモード)で開く
if((fp = fopen("out.bin", "wb")) == NULL)
{
perror("out.bin");
exit(EXIT_FAILURE);
}

//fpが参照するファイルにstringの参照する内容を書き込む
fwrite(string,sizeof(char), sizeof(string), fp);
fwrite(string2,sizeof(char), sizeof(string2), fp);

if(fclose(fp) == EOF)
{
perror("out.bin");
exit(EXIT_FAILURE);
}
return 0;
}



// read_binary.c
#include<stdio.h>
#include<stdlib.h>

int main(void)
{
FILE *fp;
char string[18]; 
char string2[8]; 

if((fp = fopen("out.bin", "rb")) ==NULL)
{
perror("out.bin");
exit(EXIT_FAILURE);
}

if(fread(string , sizeof(char), 18, fp)!=18)
{
perror("read_binary");
exit(EXIT_FAILURE);
}
if(fread(string2, sizeof(char), 8, fp)!=8)
{
perror("read_binary");
exit(EXIT_FAILURE);
}

if(fclose(fp)==EOF)
{
perror("read_binary");
exit(EXIT_FAILURE);
}

printf("%s%s\n", string, string2);
return 0;
}


$ gcc -o write_binary.c
$ gcc -o read_binary.c


#write_binaryによって生成されたファイルはlessで開くと文字化けしてしまう。
$ less out.bin
We are the world.^@penguin^@

#rea_binaryならちゃんと読み込むことができる。
$ ./read_binary
We are the world.penguin



ライブラリ関数fgetsを用いてファイルから一行ずつ文を読み込む



ライブラリ関数のfgetsはストリームから一行読み込んで第1引数のバッファに格納する。


SYNOPSIS
       #include <stdio.h>

       char *fgets(char *s, int size, FILE *stream);

以下のプログラムはこれを用いて、ファイルをコピーを実装したもの。




/* mycopy */
#include<stdio.h> //printf, perror
#include<stdlib.h> //EXIT_FAILURE, exit

#define BUFSIZE 1024
int main(int argc, char *argv[])
{
FILE *in, *out;
//bufferを用意
char buf[BUFSIZE];

//エラー処理
//コマンドライン引数が2以下の時はエラー
if(argc < 3)
{
fprintf(stderr, "Usage : copy <file1> <file2>\n");
exit(EXIT_FAILURE);
}


//コピー元を読み込みモードで開く
if((in = fopen(argv[2],"r"))==NULL)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//コピー先を読み込みモードで開く
//wはファイルが無い場合に新規作成されるモード
if((in = fopen(argv[2],"w"))==NULL)
{
perror(argv[2]);
exit(EXIT_FAILURE);
}

//一行ずつファイルからbufへ読み込む
//文の末尾を示す'\0'出会ったり、BUFSIZE-1だけ読み込む
//BUSIZE-1だけしか読み込まないのは'\0'の文が予め必要だから。
while(fgets(buf, BUFSIZE-1, in) != NULL)
{
//コピー先に書き込む
fputs(buf, out);
}

//コピー元を閉じる
if(EOF == fclose(in))
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//コピー先を閉じる
if(EOF == fclose(out))
{
perror(argv[2]);
exit(EXIT_FAILURE);
}
return 0;
}

$ gcc -o mycopy mycopy.c

$ ./mycopy


$ ls
mycopy  mycopy.c
$ echo "We are the world." > file.in
$ cat file.in
We are the world.
$ ls
file.in  mycopy  mycopy.c
$ rm file.in
$ ls
mycopy  mycopy.c
$ ./mycopy
Usage : copy <file1> <file2>
$ echo "We are the world." > file.in
$ ls
file.in  mycopy  mycopy.c
$ cat file.in
We are the world.
$ ./mycopy  file.in
Usage : copy <file1> <file2>
$ ls
file.in  mycopy  mycopy.c
$ ./mycopy  file.in  file.out
$ ls
file.in  file.out  mycopy  mycopy.c
$ cat file.out
We are the world.



マクロEXIT_FAILURE

マクロ EXIT_FAILUREは、エラー処理の中で、exit()の引数として与えられることが多いかと思います。

今回は、EXIT_FAILURE及びEXIT_SUCCESSの定義を見ました。

$ /usr/include/stdlib.h


#define EXIT_FAILURE    1       /* Failing exit status.  */
#define EXIT_SUCCESS    0       /* Successful exit status.  */

なるほど。
exit(EXIT_FAILURE)はexit(1)と同義になるわけですね。

atoiの使い方

atoiはancii to integerの略だそうです。
Cの標準ライブラリの関数です。




/* add.c */
#include<stdio.h> // printf
#include<stdlib.h>// atoi

int main(int argc, int *argv[])
{
int a, b, c;

//ascii to integer
//文字列をint型の変数に変換する
// convert a string to an integer
a = atoi(argv[1]);
b = atoi(argv[2]);
c = a + b;

printf("%d + %d = %d\n", a,b,c);

return 0;
}


$ gcc -o add add.c
$ ./add 1 2
1 + 2 = 3

makefileの基本

makefileはソースコードを複数に分割して開発を行うときに重宝されます。
ファイル間の依存関係を維持しながら、部分的にコンパイルを繰り返すことができるからです。


/* main.c */
#include"myheader.h"

//外部ファイルに内容は記述
extern void aisatsu(char *string);

int main()
{
aisatsu("Hello.\n");
return 0;

}



/* myheader.h*/
#include<stdio.h>
#include"myheader.h"

void aisatsu(char *string)
{
//引数が参照する文字列を標準出力へ表示する
printf("%s", string);
}


/* myheader.h */
// nothing



以下はmakefileです。

#makefile
# compiler
CC=gcc

# Where to install
INSTDIR=/usr/local/bin

# Where are include files kept
INCLUDE=.

# Options for development
CFLAGS=-g -Wall -ansi

aisatsu: main.o a.o
$(CC) -o aisatsu main.o a.o
main.o: main.c myheader.h
$(CC) -I$(INCLUDE) -c main.c
a.o: a.c myheader.h
$(CC) -I$(INCLUDE) -c a.c
clean:
-rm main.o a.o
install: aisatsu
@cp aisatsu $(INSTDIR);
@chmod a+x $(INSTDIR)/aisatsu;
@echo "Installed in $(INSTDIR)"


#makeを実行
$ make
gcc -I. -c main.c
gcc -I. -c a.c
gcc -o aisatsu main.o a.o


#オブジェクトファイルのお掃除
$ make clean 
rm main.o a.o

#インストール
#管理者権限の必要なコマンドが含まれるため怒られる
$ make install 
gcc -I. -c main.c
gcc -I. -c a.c
gcc -o aisatsu main.o a.o
cp: 通常のファイル `/usr/local/bin/aisatsu' を作成できません: Permission denied
make: *** [install] エラー 1

#管理者権限で実行
$ sudo make install 
[sudo] password for yumiko: 
Installed in /usr/local/bin


#コマンドを実行
#/usr/local/binにはパスがデフォルトで通っているため、実行ファイル名だけを入力すればよい。
$ aisatsu
Hello.



time()の値から現在の日付を計算

ライブラリ関数のgmtime()を用いれば、timeの結果から現在の日付を算出することが可能です。


$ man gmtime

#include<time.h>

struct tm *gmtime(const time_t *timep);


           struct tm {
               int tm_sec;         /* seconds */
               int tm_min;         /* minutes */
               int tm_hour;        /* hours */
               int tm_mday;        /* day of the month */
               int tm_mon;         /* month */
               int tm_year;        /* year */
               int tm_wday;        /* day of the week */
               int tm_yday;        /* day in the year */
               int tm_isdst;       /* daylight saving time */
           };



上に示すtm型の構造体を返却してくるようである。


/* mytime.c */
//低水準の時刻から現在の時刻を計算し、表示させるプログラム
#include<stdio.h> //printf
#include<time.h> //time, gmtime
int main(void)
{
//UNIXエポック(1970.01.01.AP00:00)を起点とした時間(second)を表示
time_t mytime;
struct tm *time_ptr;

time(&mytime);

//gmtimeより現在の日付を算出
time_ptr = gmtime(&mytime);

printf("Raw time is %ld\n", mytime);
printf("date : %02d/%02d/%02d\n", time_ptr->tm_year, time_ptr->tm_mon+1, time_ptr->tm_mday);


return 0;
}



$ ./mytime2
Raw time is 1342782353
date : 112/07/20

112年って。。。。2000年問題が起こっています笑





UNIXエポックからの経過時間



time関数はシステムコールの一つです。
//UNIXエポック(1970.01.01.AP00:00)を起点とした時間を秒を単位として返します。

$ man 2 time

SYNOPSIS
       #include <time.h>

       time_t time(time_t *t);

DESCRIPTION
       time()  returns  the  time  since the Epoch (00:00:00 UTC, January 1, 1970), measured in seconds.




/* mytime.c */
//低水準の時刻を表示するプログラム
#include<stdio.h> //printf
#include<time.h> //time
int main(void)
{
//UNIXエポック(1970.01.01.AP00:00)を起点とした時間(second)を表示
time_t mytime;
time(&mytime);

printf("The time is %ld\n", mytime);
       
        //UNIXエポックからの経過時間を自分で計算する
int mytime2;
mytime2 = (2012-1970) * 365 * 24 * 60 * 60;
printf("The time is %d\n", mytime2);

return 0;
}



$ ./mytime
The time is 1342781355
The time is 1324512000

Advanced Programming in the UNIX Environmentのヘッダーファイルをダウンロードする

Advanced Programming in the UNIX Environmentのフォローアップサイト
http://www.yendor.com/programming/unix/apue/apue.html

#Linuxならwget
$ wget http://www.yendor.com/programming/unix/apue/lib.svr4/ourhdr.h


#Mac OS Xならcurlコマンドを使う。-Oオプションが必要
$ curl -O http://www.yendor.com/programming/unix/apue/lib.svr4/ourhdr.h

#中身を見る
$ less ourhdr.h

bashが親プロセスであることを確かめるプログラム

ターミナルでプログラムを呼び出すときに、親プロセスとなるは当然shell(bash)です。
シェルは内部でfork()を用いて、プロセスを複製し、その子プロセスにおいて新規のプロセスを呼び出しているのです。

今日は、システムコールgetppid()を用いて、親プロセス(すなわちbash)のプロセスIDを取得するプログラムを書きました。

$ vim parent_process.c

/* parent_process.c */
#include<stdio.h> //printf
#include <sys/types.h> //getppid, getpid
#include <unistd.h> //getppid, getpid

int main(void)
{
printf("My process ID is %d\n", getpid());
printf("My parent PID is %d\n", getppid());

return 0;
}


#コンパイル
$ gcc -o parent_process parent_process.c

#現在、動いているPIDを表示
$ ps
  PID TTY          TIME CMD
 3657 pts/0    00:00:00 bash
 3702 pts/0    00:00:00 less
 4973 pts/0    00:00:00 ps
$ ./parent_process 
My process ID is 4983
My parent PID is 3657

親、子それぞれのプロセスID


このブログでforkの基本的な使用方法について幾度か書いたことがあります。

今回は、親、子各々のプロセスから自分の環境におけるプロセスIDを表示させるプログラムを作りました。

fork()というシステムコールはまったく同じプロセスを複製するわけですが、その際に親と子でもで戻り値が異なることが最大のミソです。
戻り値をチェックして、if文を使って親と子それぞれの下流の処理を記述してあげればいいのです。

// fork_test.c
#include<sys/types.h> //fork
#include<stdio.h> //printf
#include<stdlib.h> //exit

int main(void)
{
int pid;
if((pid = fork()) < 0){
perror("fork_test");
exit(1);
}
else if((pid == 0)){
//child processには0が戻る
printf("I am the child process.My process ID is %d\n", pid);
return 0;
}
else{
    //parent processには子のプロセスIDが戻る
printf("I am the parent process.My process ID is %d\n", pid);
}

return 0;
}




$ gcc -o fork_test fork_test.c


$ ./fork_test
I am the parent process.My process ID is 3412
I am the child process.My process ID is 0







少しく工夫して、親、子のプロセス各々でシステムコールのgetpidを用いて、自らのPIDを取得させるとともに、if文の中で実行されたforkの戻り値を表示させるプログラムを作りました。
子プロセスではさらに、getppid(get parent process id)というシステムコールにより、親のPIDを取得し、表示させています。



// fork_test_2.c
#include<sys/types.h> //fork
#include<stdio.h> //printf
#include<stdlib.h> //exit


#include <sys/types.h> //getpid, getppid
#include <unistd.h> //getpid, getppid

int main(void)
{
int pid;
if((pid = fork()) < 0){
perror("fork_test");
exit(1);
}
else if((pid == 0)){
//getpid()によりProcess IDを取得、表示
printf("I am the child process.My process ID is %d\n", getpid());
//child processには0が戻る
printf("My pid variance is %d in my environment.\n", pid);
//getppidにて、親プロセスのprocess IDを調べる。
printf("My parent process ID is %d\n", getppid());
return 0;
}
else{
//getpid()によりProcess IDを取得、表示
printf("I am the parent process.My process ID is %d\n", getpid());
//parent processにが子プロセスProcess IDが戻る
printf("My pid variance is %d in my environment.\n", pid);
}

return 0;
}












ファイル記述子、ファイル型、ファイルポインタ、stdin、stdout、stderrについての考察


C言語の文法的な意味においては
ファイル記述子はファイルポインタが参照する構造体(ファイルシステムのiノードリストに含まれる利用者ファイル記述子表の構造体)の持つ要素のうちの一つ(int _fileno)です。

FILE型の定義を調べてみます。

$ less -N /usr/include/stdio.h


     48 /* The opaque type of streams.  This is the definition used elsewhere.  */
     49 typedef struct _IO_FILE FILE;


なるほど。48-49行目のところでFILE型の実体は、_IO_FILEという構造体であることがわかります。

_IO_FILEは、stdio.hがインクルードしているヘッダファイルの/usr/include/libio.hに記述されています。それを見てみます。

$ less -N /usr/include/libio.h


    271 struct _IO_FILE {
    272   int _flags;           /* High-order word is _IO_MAGIC; rest is flags. */
    273 #define _IO_file_flags _flags
    274
    275   /* The following pointers correspond to the C++ streambuf protocol. */
    276   /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
    277   char* _IO_read_ptr;   /* Current read pointer */
    278   char* _IO_read_end;   /* End of get area. */
    279   char* _IO_read_base;  /* Start of putback+get area. */
    280   char* _IO_write_base; /* Start of put area. */
    281   char* _IO_write_ptr;  /* Current put pointer. */
    282   char* _IO_write_end;  /* End of put area. */
    283   char* _IO_buf_base;   /* Start of reserve area. */
    284   char* _IO_buf_end;    /* End of reserve area. */
    285   /* The following fields are used to support backing up and undo. */
    286   char *_IO_save_base; /* Pointer to start of non-current get area. */
    287   char *_IO_backup_base;  /* Pointer to first valid character of backup area */
    288   char *_IO_save_end; /* Pointer to end of non-current get area. */
    289
    290   struct _IO_marker *_markers;
    291
    292   struct _IO_FILE *_chain;
    293
    294   int _fileno;
    295 #if 0
    296   int _blksize;
    297 #else
    298   int _flags2;
    299 #endif
    300   _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
    301 
    302 #define __HAVE_COLUMN /* temporary */
    303   /* 1+column number of pbase(); 0 is unknown. */
    304   unsigned short _cur_column;
    305   signed char _vtable_offset;
    306   char _shortbuf[1];
    307 
    308   /*  char* _save_gptr;  char* _save_egptr; */
    309 
    310   _IO_lock_t *_lock;
    311 #ifdef _IO_USE_OLD_IO_FILE
    312 };


なんだか壮大なスケールの構造体の定義でしたが、目的のint_filenoを見つけ出すことができました。



ここで、あらかじめ定義されているファイルポインタstdin, stdout, stderrの定義内容を見てみます。


$ less -N /usr/include/stdio.h

    164 /* Standard streams.  */
    165 extern struct _IO_FILE *stdin;          /* Standard input stream.  */
    166 extern struct _IO_FILE *stdout;         /* Standard output stream.  */
    167 extern struct _IO_FILE *stderr;         /* Standard error output stream.  */

なるほど。stdin, stdout, stderrは_IO_FILE型の構造体を参照するポインタとして定義されているのですね。

以上のことより、下のようなプログラムを作ることができます。


// test.c
#include<stdio.h> /* printf, stdin, stdout, stderr */

int main(void)
{
//ファイルポインタの宣言
FILE *fp;
fp = fopen("./inputfile", "r");

/*各々のファイルポインタが参照するFILE型の構造体の_filenoを
 表示させる。
 _filenoとはファイルシステムのiノードリスト内に存在する
 iノードリトにある、「利用者ファイル記述表」というFILE型の構造体
 が連結されたリストの順番を示す番号である。  */

/*stdin, stout, stderrはstdio.hで定義されている
 ファイルポインタである。*/
printf("stdin  : %d\n", stdin->_fileno);
printf("stdout : %d\n", stdout->_fileno);
printf("stderr : %d\n", stderr->_fileno);
printf("fp     : %d\n", fp->_fileno);

//ファイルのクローズ
close(fp);


return 0;
}

#コンパイル
$ gcc -o test test.c

#実行
$ ./test 
stdin : 0
stout : 1
stderr: 2
Segmentation fault

#inputfileという空ファイルを作成
$ touch inputfile

#3番のファイル記述子がちゃんと割り当てられていることが確認できる。
$ ./test 
stdin : 0
stout : 1
stderr: 2
fp    : 3

libc.aの中身を暴く

libc.aと言えば、普段UNIX環境でプログラミングを行うときにルーチンでリンクさせているライブラリのことです。

libc.aは実は独立した機能を持ったオブジェクトファイル(システムコールや、システムコールを呼び出すことによって実現される標準入出力関数など)がアーカイブされたものなのです。

今回は、libc.aを適当なディレクトリにコピーしてきて、そこで解凍してみて中身を検証してみました。

#libc.aのコピー
#libc.aを壊すと面倒なのでコピーしてしまうのである。

$ cp /usr/lib/libc.a .


#解凍
$ ar  -x libc.a 

#生成したオブジェクトファイルは1510個もある!
$ ls | wc
   1510    1510   17779

#代表的なシステムコールは含まれているか!?
$ ls -l open.o creat.o close.o read.o write.o lseek.o unlink.o 
-rw-r--r-- 1 kappa kappa 1136 2012-07-15 21:46 close.o
-rw-r--r-- 1 kappa kappa 1120 2012-07-15 21:46 creat.o
-rw-r--r-- 1 kappa kappa  932 2012-07-15 21:46 lseek.o
-rw-r--r-- 1 kappa kappa 1156 2012-07-15 21:46 open.o
-rw-r--r-- 1 kappa kappa 1156 2012-07-15 21:46 read.o
-rw-r--r-- 1 kappa kappa  892 2012-07-15 21:46 unlink.o
-rw-r--r-- 1 kappa kappa 1160 2012-07-15 21:46 write.o

#代表的な標準入出力関数はあるか!?
$ ls -l scanf.o fprintf.o
-rw-r--r-- 1 kappa kappa  996 2012-07-15 21:46 fprintf.o
-rw-r--r-- 1 kappa kappa 1008 2012-07-15 21:46 scanf.o
kappa@kappa:~/test/artest$ 


libc.aは頻繁に使用される関数のオブジェクトファイルを内包しているという点でとても大切であることがよくわかりました。

SYSCALLS(2)

Linuxの全システムコールのリストを得たいときは、SYSCALLS(2)という第2章の目次のような項目が参考になります。

$ man syscalls



NAME
       syscalls - Linux system calls

SYNOPSIS
       Linux system calls.

DESCRIPTION
       The system call is the fundamental interface between an application and the Linux kernel.


       System call                 Kernel        Notes
       ────────────────────────────────────────

       _llseek(2)                  1.2
       _newselect(2)
       _sysctl(2)
       accept(2)
       accept4(2)                  2.6.28
       access(2)
       acct(2)
       add_key(2)                  2.6.11
       adjtimex(2)
       afs_syscall(2)                            Not implemented
       alarm(2)
       alloc_hugepages(2)          2.5.36        Removed in 2.5.44
       bdflush(2)
       bind(2)
       break(2)                                  Not implemented
       brk(2)
       cacheflush(2)               1.2           Not on i386


以下続く。


システムコールの総数が定義されているのは、/usr/include/asm/unistd_32.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
以下続く。

ちなみに僕の環境(Ubuntu10.04、32bit)では、システムコールの総数は337個でした。







main()の第三引数の検証

main()には環境変数を格納する第三の引数(*envp[])が存在します。
*envp[]は環境変数を格納する文字列に対してのポインタ配列です。

以下のプログラムでは、envp[]が参照する配列の中身を順次表示させるものです。


/* myenv.c */
/* 第3の引数を検証するプログラム*/
#include<stdio.h> /* printf */

/*
main(int argc, char *argv[], char *envp[])
int argc;
char *argv[];
char *envp[];

*/


int main(int argc, char *argv[],char *envp[])
{
int i;
/* env[i]が存在する限り繰り返す */
for(i=0; envp[i]; i++){
printf("%s\n", envp[i]);
}

return 0;
}

gccが行っている作業を検証してみる

GCCはいくつかのコマンドによって構成されているらしいのである。

普段のgcc -o outfile infileと入力するだけだが、裏では様々なイベントが発生している。

gccは、

プリプロセッサ: /lib/cpp
コンパイラ : /usr/lib/gcc/i686-linux-gnu/4.4/cc1
アセンブラ : /usr/bin/as
リンカ: /usr/bin/lib

の4つのコマンド適宜呼び出すための、ドライバとしての働きをします。

以下に、ここのコマンドを個別に呼び出して、実行形式ファイルを作成する方法を示します。

#1.ソースコードの作成
$ vim hello.c

#include<stdio.h>

int main(void)
{
printf("Hello, world!\n");
return 0;
}

#2.プリプロセス(#で始まる内容の展開;ヘッダーファイルの取り込み、マクロの展開)
#プリプロセスの結果は標準出力に出されるので、リダイレクトしてファイルに出力する。
/lib/cpp hello.c > hello.i

#3.プリプロセスにより生成した中間ファイルをアセンブリソース(hello.s)にコンパイル
/usr/lib/gcc/i686-linux-gnu/4.4/cc1 hello.i


#4.アセンブラによりアセンブルを行いオブジェクトファイル(hello.o)を生成
$ /usr/bin/as hello.s -o hello.o

#5.オブジェクトファイル、共有ライブラリファイル(libc.so)、スタートアップルーチン(/usr/lib/crt1.o)等をダイナミックリンクさせ、実行形式ファイル(hello)を生成
$ /usr/bin/ld  -o hello -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/libc.so /usr/lib/crtn.o hello.o

#6.実行形式ファイルを実行
$ ./hello
Hello, world!


理解すると非常にすっきりできるお話です。コンパイルは、入門書には非常に観念的なレベルのみ説明されていることがほとんだと思います。

今回のように、一つ一つんファイルをフルパスで指定していくと、コンパイルの本質が見てきます。

ちなみに、同様の作業は、gccに特定のオプションをつければ可能です。











ld -o hello -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o hello.o -lc /usr/lib/crtn.o