時代の転換点に差し掛かっている昨今ですが、技術は我々を裏切らねー!!(資本主義的には裏切ることがあります)ということで前回に引き続き、C言語プログラムについて考察していこうと思います。
前回は、Visual studioを用いてメモリの番地を見つつ、ポインタのあり方を垣間見たわけであります。
しかし、Visual Studioにも問題点がありまして、短いプログラムをやる際には、色々と面倒くさいという問題があるのです。
大した作業ではないのですが、いちいち作業フォルダを作らないといけないので、フォルダの量がべらぼうに増えていく・・・
もう少し簡単な勉強環境を構築したいというわけで、今回はWindows10から標準装備されたubuntu linuxを利用して、ささっとC 言語をやろうというわけであります。
とはいうものの、わたくしなんぞは、gcc+emacsでプログラムをやったりはしたことあるものの、詳細なデバッグをgccでやったことはありませんでした。
こういう環境の場合、どうやってデバッグするの?と思い調べてみた所、gdbなるものがあることが判明したのでこいつを使ってみることにしました。
Linuxて面倒なんじゃないの??という声が聞こえてきそうですが、実際初めてやる人にとってみれば最初は面倒くさいかもしれません。
けれど、現在ではWindowsで出来るので、めちゃ楽ですしちょろっとコマンドを覚えれば、勉強するには楽やな・・・と思うはずです!
gdbに関しては、この文章を書いてる時点で、初めて触っております。
とはいえ、有名なものらしく学んでおいて損はなさそうだ、という判断で使ってみることにしました。
ポインタのことを追求したいので、今回の目的を以下の2点にしました。
1. gdbで簡単なデバッグを出来るようになる(主に値やアドレスを見る)
2. アセンブラもgdbで表示できるようにする
目次
環境構築からgccによるコンパイルまで
windows10にubuntu・gcc・gdbのインストール
はじめにインストール方法を超簡単に説明します。
まずは、ウィンドウズ10にubuntu linuxをインストールします。
1. Microsoft Storeを開き、検索欄でubuntuと入力。

2. ubuntuが出てくるので、一番左のubuntuをクリック。

3. 入手ボタンをクリック
ダウンロードし終えたら、早速起動します。
しかし、wslregisterdistribution failed with error: 0x8007019e
という謎のエラーが生じて動きませんでした。
エラー内容を検索してみると、Power Shellをいじればいれらるらしい。

4. 左下の、「ここに入力して検索」にPower Shellと検索するとアイコンが出てくるので、右クリックして「管理者として実行」をクリック。
5. Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
をコマンドラインに入力してクリック。

すると、再起動するかどうか聞いてきますので、再起動します。
再起動後、ubuntuを起動してみると以下の画面に。
どうやら、動作に成功したみたいです。
6. username とパスワードを入力してエンター。

すると、ubuntuが動き始めます。
これからやっとgccとgdbをインストールします。
7. sudo apt get install gccをコマンドラインに入力するとパスワードを聞かれます。これに答えればインストールされます。

完了したら、gccと同様にgdbもインストールします。
7. sudo apt get install gdbをコマンドラインに入力しエンター。
これで、gccとgdbのインストールが終了しました。
エディタemacsをインストール
エディタは何でもいいのですが、ここは簡単に使えるという理由でemacsにしましょう。
インストール方法は、自分で書くよりいいサイトが多々あるのでそちらを紹介しますが、コマンドラインに3つのコマンドを順番に入れただけです。
$ sudo add-apt-repository ppa:kelleyk/emacs
$ sudo apt-get update
$ sudo apt install emacs26
これでとりあえず、emacsもインストール出来ました!
emacsを使ったプログラムファイルの作成
ファイルの作成
コマンドライン上で、emacs 作りたいファイル名、で新しいテキストファイルを作成することが出来る。
$ emacs test.c
とすれば、test.cというファイルを作成し、さらにemacsで開くことが出来る。
サンプルプログラム(ポインタの基礎プログラム)
さて、ここでポインタを含む今回のプログラムtest.cを載せておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> void convert(char *); main() { char string[] = "characters"; printf("変換前の文字は: %s\n", string); convert(string); printf("変換後の文字は: %s\n", string); } void convert(char *s) { while(*s != '\0'){ if(*s >='a' && *s <= 'z') *s -= 32; ++s; } } |
簡単に言いますと、小文字を大文字へ変換するプログラムになるのですが、文字の受け渡しにポインタを使っております。
emacsでこのプログラムを打てたなら、早速保存して、この環境から抜けます。
M-xはEscキーを押した後にxを押すということを表しています。
終了 : Ctrl-x Ctrl-c
行数を表示 : M-x linum-mode
さて、これを早速gdbでデバッグしてみましょう!
コンパイル(gcc)とデバッグ(gdb)
gccによるcプログラムのコンパイル
gccによりコンパイルするのですが、今回はgdbでデバッグするので-gオプションを付けます。
-gでデバッグ情報を、-Oは最適化オプションらしいです。
これ付けないでデバッグすると、確かに不具合が起きました。
-O0とすることで、最適化を無効にすることが出来ます。
-gをつけないと、gdbによるデバッグが1行ずつ行えず、何が悪いの??とハマることになるので、コンパイル時のオプションはお忘れなきよう!(僕は、ハマりました)
gdb起動 : gdb ./a.out
tmuxの方が、青色が薄くなり、非常に見やすくなりました!

gdbのコマンド
gdbによるデバッグは慣れないと結構難しい、というか初めて使ったので未だその能力の10分の1も引き出せずにいます。
なので、ここでは使うであろう最低限のコマンドを説明しておきます。
初学の場合、出来るだけシンプルが良いので、とにかく最低の数のコマンドで乗り切りたいと思います。
さて、上記gdb a.outで無事gdbを起動出来たらいよいよデバッグすることが出来ます。
コマンドラインの左が、(gdb)となっていたら、成功しています。
ブレークポイント
Visual studioでは、直感的にブレークポイントを設置出来たのですが、こいつはそうではありません。コマンドを打ち込んでブレークポイントをしかける必要があります。
2行目にしかける場合は、breakpoint 2行目を略した以下のコマンドでしかけられます。
(gdb)b 2
main関数から調べたいときなどは多いと思いますが、その場合は
(gdb)b main
このブレークポイントを消したい場合、delete breakpointの略で
(gdb)d br
今、設置しているブレークポイントを確認したいときは、information breakpointの略で
(gdb)i b
プログラムの実行とステップ
ブレークポイントをかけたら、プログラムを実行させます。
すると、ブレークポイントがあるところで止まるわけです。
プログラムの実行は、runの略で
(gdb)r
その次の行を一行ずつ実行させて、プログラムを見ていきます。
一行ずつ実行するコマンドはnextの略で
(gdb)n
stepとnextでプログラムの実行法に違いがあるのですが、ここはもうnだけを使います。
詳しく知りたい方は、ググればすぐに違いを調べることが出来ます。
変数の表示
変数の表示はだいぶポインタの理解の助けになりました。
というのは、&をつければ変数のアドレスを見ることが出来ますし、*でアドレス先の値を読むことが出来ます。
まさにC言語の文法そのものなので、これは使わない手はないというわけです。
(gdb)p 変数名 : 変数の値を読み込む
(gdb)p &変数名 : 変数のアドレスを読み込む
(gdb)p *変数名 : ポインタの値を読みだす
アセンブル表示
アセンブルも色々コマンドがあるのですが、ここでは一つに絞ります。
(gdb)layout asm
これで、アセンブラを直接見ることが出来るので、左にC言語、右にアセンブラでデバッグすることが可能になります。
以上のコマンドを使って、デバッグしてみましょう!
プログラムのデバッグ
このプログラムは、ざっくり説明すると小文字のcharactersをconvert関数で大文字のCHARACTERSにして表示するというものです。
char型の文字列をポインタで飛ばす、convert関数がきもとなっておるわけです。
そこら辺を雰囲気頭に入れて早速デバッグしてみます。
まずは、メイン関数にブレークポイントをしかけて、プログラムをrunさせます。
そこからは一行ずつnでデバッグしてみましょう!
(gdb)b main
(gdb)r
画面的にはこんな感じになるかと思いますが、皆様いかがでしょうか?

ずっとnをエンターさせてみると、convert関数に行かずにそのままプログラムが終了してしまうと思います。
とりあえず、これでプログラムがきちりと動作していることが確認出来たかと思います。
ポインタのプログラムなので、変数とアドレスの関係を追えばよいということなので、10行目にブレークポイントをしかけてみます。
7~8行目
ここで、stringにchar型のcharactersを代入するわけです。
早速ここで、p stringとコマンドを打ちまして、中身を見てみると、””\000\000\000\000\342\376\377\377\177\000″ “が入っていることがわかりました。この段階ではまだ、代入されてないということなのか!?
ということで、nを入力して8行目でp stringしますと、
こいつのアドレスをp &stringで調べておきましょう!
アドレスは、青色で見にくいですが、0x7ffffffee0fdでした。

ここで、分かった気になれないのが配列とアドレスの関係です。
配列のアドレスと言っても配列には何個もデータが入っているわけで、よくイメージが掴めないというのが本音です。
ついでにp *stringでポインタを見てみますと、まさかのc!!
‘characters’の最初の文字のcだと思われます。
これは本などに載っておりまして、配列名そのものが、配列の先頭アドレスを現しているということを表しているのでは?
先頭であるstring[0]の中身を*により間接参照したということでしょう。

簡単に確認する方法があることに気づきました。(これ以降写真は見にくい気がしたので結果も普通に打ち込みます)
$19 = 0x7ffffffee0fd “characters”
(gdb) p string[0]
$20 = 99 ‘c’
ほほう!!!!
配列の0番目のアドレスが0x7ffffffee0fdでその値は99 ‘c’となっております。
stringは、配列の0番目のアドレスを指していたということは間違いありません。
この99というのはアスキーコードにおける10進数では99という意味であります。
char型ですと’c’になるのです。アスキーコードと数字の対応表を載せておきます。
以下の表に示すように、各文字がDec(10進数)やHex(16進数)に対応しているわけです。
僕はこのことも最初知らなかったので、初めはいたく感動しましたが、今やこの表をよく覗いております。
出典:http://www.ie.u-ryukyu.ac.jp/~e085739/c.tuts.ascii.html

話を元に戻して、配列の0番目にcが入っていることがわかりました。
そうすると、その次のアドレスにhが入っているのか?!と気になります。
その次の配列とアドレスを見てみましょう!
(gdb) p string[1]
$22 = 104 ‘h’
(gdb) p &string[1]
$23 = 0x7ffffffee0fe “haracters”
はーん!!
string[1]にはh(表を見て10進数で104になっていることを確認します)が入っており、そのアドレスは0x7ffffffee0feになっております。
16進数で1(0x01と書きます)進んだ値になっていることから、順々に値が格納されていることが予測できます。
もう一つアドレスを進めて確認しておきます。
(gdb) p &string[2]
$24 = 0x7ffffffee0ff “aracters”
d→e→fと末尾が0x01ずつ変化しているので、間違いありません。
これまでの結論を、図にすると以下のようになります。
最後の’s’のアドレスは末尾が106になります。

16~19行目
convert関数に入った後の変数を調べてみましょう。
*sとしてstringを受け取ります。
早速、p sとして中身を見てみましょう。
(gdb) p s
$27 = 0x7ffffffee0fd “characters”
ほほーッ!!
配列の先頭アドレスがそのまま運ばれてきているようです。
そこに*をつけるので、そのアドレスの中の値を運んできているということですね。
(gdb) p *s
$28 = 99 ‘c’
*sすると、配列の0番目の値が出てきますので間違いありません。
sはアドレスを運んでいるのですね!
while文で*s != ‘\0’となっておりますが、’\0’はヌル文字と呼ばれ、文字を区切るときに使われます。
文字の最後につけるモノということでとりあえず把握しておきましょう。
charactersの後に’\0’がくるので、こいつが来たらループを終了させるということです。
if文ではa以上z以下となっておりなんじゃ?と思うかもしれませんが、こいつもアスキーコード表を見ていただければ解決します。
aは97、zは122なのでこの間の値にある文字ならオーケーということになります。
cは99なので、この条件を満たしているということになります。
*s -= 32の部分で大文字にしております。
これもアスキーコード表を見ればわかりますが、aは97に対し、Aは65になっております。
すなわち、-32すると大文字になるというわけです。
そして最後、++sでアドレスを一つずらして、hにするわけです。
そして、hをHにしたら、次のアドレスと、順繰りやっているわけです。
実際調べてみると、以下のようになり正しいことがわかりました。
(gdb) p *(s+1)
$30 = 104 ‘h’
こうしてみると、なかなか興味深い値の運び方であることがわかります。
値そのものを変化させているというより、アドレスを受け取って、*により中身を参照しているといった手法なのですね。
次に、アセンブルを表示させる方法だけ書いておきます!
アセンブル表示
gdbによるアセンブル表示
最後に、解析手法の一つとして、アセンブルの表示方法も載せておきます。
最初にコマンド解説で行った、layout asmをgdbコマンドラインに入力してみましょう。
すると以下のような表が出てくるはずです。
こいつが出てきたら成功です。
この表示は、main関数の部分を表していることがわかります。
一番左がアドレスになっており、mainの関数からどれくらいアドレスがずれたかを+〇〇として表示しています。
pushとかが、アセンブラ命令ですね!
%rspとかはレジスタを表しています。

このまま、上と同じ方法でブレークポイントをしかけrunさせればアセンブラを追うことが出来ます。
bashコマンドによるアセンブラ表示
gdbを終了させ、ubuntuのコマンドラインに
下に表示を進めていくとmainの部分があるかと思います。
gdbで出したアセンブラと同じであることが分かります。
これらを見比べて解析することも可能ということですので、いずれアセンブラ解析も一回は行いたいと思っています。

というのは、まだ中身を全然理解できていないので、解説記事が書けないという問題があるのです。笑
まとめ
今回は主に、gdbの使い方とポインタの説明をやっていきました。
前回、Visual Studioを使ったポインタの説明をしましたが、ことポインタに関して言えば、gdbの方が遥かにマスターしやすいのではないか?と感じております。
gdbデバッガによる&や*がC言語の文法と同じなので、直接値を確かめることで、理解しやすくなるというのが重要なポイントです。
確かに使うのが最初は面倒ですが、無料ですしぜ是非とも使ってみることをお勧めします!

閑話休題:「技術の深さ」と「会社の利益」および「給料」の関係について
その人個人にとっては凄い良いことなのですが、経営者から見て、ある社員がそのことを学ぶことが会社の利益に適っているのか?ということです。
私の会社はある特化した技術領域で深い知見と経験を持っており、それを製品として売っています。
仕事の特性上、様々な技術領域を網羅せねばならないのですが、一つのことにはまってしまうと奥まで掘ってしまい、時間がかかるという問題があります。(その意味では、引きこもって研究してたらいかに進むだろうか・・・と考えずにはいられません。。。)
一方で、会社という組織は利益を生む行為が最も重要であって、技術を深堀することは、利益を上げるという点においては必ずしも善ではない、ということについて私は重々理解しております。
そのため、限りある時間の中で、取捨選択して学んで行く必要があるんだろうなぁ・・・ということなのです。
このことから、逆説的に会社が求めているスキルを逐次マスターし、他の社員との差異を作っていくことが、個々人の給料を上げる一つの最も早い方策なのではないか?と考えております。
無論、経営者からすると固定費のコストを抑えたいはずなので、人件費を上げるというのは非常に慎重にならざるを得ません。
しかし、会社にとって必須な人材になれれば、辞めてしまう可能性を考慮して給料を上げざるを得ないでしょう。技術系の人なら、尚のことです。
何故なら、ある個人が技術を蓄えまくって、技術の総量がある閾値を超えた場合、突出した才の持ち主として、他社でもあるいは個人でもやっていける可能性があるからです。
ソフトウェアエンジニアリングの場合、このような状況になると大手でも辞めていく人がかなり多いようです。
僕が人を雇うとしたら、何でもある程度できる人には給料をかなりあげると思います。
その代わり、多くの人は雇わず、少数で最大の利益を得るための効率化をはかります。
既に満ち足りしているし、どこでも同じ製品が作れるようになってしまうのです。