このページでは、VSCode で『起動済みのプロセス』に対してデバッグを行う方法について説明します!
プロセスという言葉が苦手であれば、『既に実行中のプログラム』に対するデバッグ方法と考えても良いと思います。
このサイトでは、以前に下記ページで VSCode でプログラムをデバッグする方法について解説しています。少し難しい言い方をすると、下記ページで解説している方法は、指定したプログラムを実行した際に生成されるプロセスに対してデバッグを行う方法になります。
VSCodeでMacOSにC言語デバッグ環境を構築それに対し、このページでは既に起動しているプロセスに対してデバッグを行う方法について解説を行なっていきます。
今回は、Mac 上でC言語プログラムのプロセスに対するデバッグ方法に基づいて解説をしていきますが、おそらく、考え方や設定方法に関しては他の環境や言語でも同様になると思います。
また、今回は起動済みのプロセスに対するデバッグに焦点を当てて解説していきますので、VSCode でのデバッガーの使用方法やビルドの設定等は上記ページを参考にしていただければと思います。
Contents
起動済みのプロセスをデバッグする方法
では、起動済みのプロセスをデバッグする方法について解説していきます。
ポイントは、デバッガーにプロセスを起動させるのではなく、既に起動しているプロセスへデバッガーが接続しに行くようにデバッグ構成を設定する必要がある点になります。このようにデバッガーから接続を行うことをアタッチと呼びます。
"request"
に "attach"
を指定する
下記ページでも解説しているとおり、VSCode ではデバッグの構成は launch.json
というファイルで設定可能です。
例えば、CodeLLDB というプラグインを使用し、デバッガーからプログラムをデバッグする際には下記のようなデバッグ構成を用意することになります。
"configurations": [
{
"name": "c_debug",
"type": "lldb",
"request": "launch",
"program": "${fileDirname}/${fileBasenameNoExtension}",
"args": [],
"cwd": "${fileDirname}",
"preLaunchTask": "c_build"
}
]
上記の c_debug
構成を用いてデバッグを開始すると、"program"
オプションで指定したパスのプログラムが実行されてプロセスが生成され、そのプロセスに対してステップ実行等のデバッグ操作を行うことができるようになります。
ポイントは "request"
オプションの指定で、ここに "launch"
を指定した場合、デバッグ開始した際にはデバッガーが "program"
で指定したパスのプログラムを実行し、それによって起動したプロセスに対してデバッグが行われることになります。
それに対し、起動済みのプロセスをデバッグする場合は、"request"
に "attach"
を指定する必要があります。"request"
に "attach"
を指定することにより、デバッガーがプログラムを実行することなく、デバッガーが直接プロセスにアタッチ(接続)しに行くようになります。
上手くアタッチできれば、そのプロセスのデバッグを行うことができるようになります。
つまり、"request"
に "launch"
を指定した場合、"program"
で指定したパスのプログラムが実行され、それによって生成されたプロセスに対してデバッグを行うことになります。そのため、デバッグ対象は "program"
で指定したパスのプログラム実行によって生成されたプロセスに限られます。
それに対し、"request"
に "attach"
を指定した場合、プログラムの実行は行わず、既に生成済みのプロセスに対して直接アタッチしてデバッグを行うことになります。アタッチに成功するかどうかは置いておいて、アタッチ先のプロセスは自由に選択することができます。
ただし、"request"
に "launch"
を指定する時と同様に、ステップ実行等を行う場合には、デバッグ対象となるプログラムのソースコードが必要となります。
また、C言語などのコンパイル型言語の場合、デバッグ対象となるプログラムにはデバッグ情報が埋め込まれている必要があります。つまり、デバッグ対象となるプログラムをコンパイルする際には -g
オプションを指定しておく必要があります(-O0
を指定しておくと最適化が OFF されてステップ実行もやりやすくなります)。
スポンサーリンク
"pid"
や "processId"
に "${command:pickProcess}"
を指定する
前述のように "request"
に "attach"
を指定することでプロセスにアタッチすることが可能になるのですが、アタッチ先のプロセスはユーザーが指定する必要があります。
"request"
に "launch"
を指定した場合、実行するプログラムのパスを "program"
に指定しておけば、そのプログラムの実行により起動するプロセスが自動的にデバッグ対象として選択されるので分かりやすいです。
では、"request"
に "attach"
を指定した場合は、アタッチ先のプロセスをどのように選択すれば良いでしょうか?
結論としては「プロセス ID」を指定してやれば良いです。プロセスには ID (プロセス ID) が割り振らており、この ID により各プロセスを識別することができるようになっています。そのため、このプロセス ID をデバッガーに指定することでアタッチ先のプロセスを選択することが可能です。
デバッガーにプロセス ID を指定できるようにするためには、デバッグ構成に "pid"
オプションや "processId"
オプションを追加する必要があります。
利用可能なオプション名は使用するプラグインによって異なります。
例えば、CodeLLDB プラグインの場合は "pid"
オプションが利用可能ですが、C/C++ プラグインの場合は "processId"
オプションを利用する必要があります
これらのオプションにプロセス ID を指定してやれば、デバッグ開始時にデバッガーがその ID のプロセスにアタッチしに行くようになります。
ただ、これらのオプションには直接プロセス ID を指定するのではなく "${command:pickProcess}"
を指定するのが便利だと思います。
これにより、デバッグ開始時にプロセス ID 一覧が表示されるようになり、その一覧からアタッチしたいプロセスの ID を選択して指定することができるようになります。
ただ、上の図を見ていただいても分かる通り、起動中のプロセスは PC 上に多く存在しており、プロセス ID 一覧が表示されてもアタッチしたいプロセス ID を見つけ出すのは結構大変です。可能であればデバッグしたいプログラムのソースコードを変更してプロセス ID を表示するようにしてやるのが楽だと思います。
例えば Mac や Linux の場合、getpid
関数を実行することでプロセス ID を取得することができます。getpid
関数するためには unistd.h
をインクルードする必要あります。
何かしらの方法でプロセス ID を特定できるようにしてやれば、あとはデバッグ開始時に表示されるプロセス ID 一覧からその ID のものを選択してやれば良いだけになります。
その他のオプションの指定について
"request"
に "attach"
を、"pid"
や "processId"
に "${command:pickProcess}"
を指定したデバッグ構成を用意すれば、デバッグを開始するとプロセス ID の一覧画面が表示され、そこで選択した ID のプロセスにデバッガーがアタッチしに行くことになります。
ただ、デバッグ構成には上記以外にも様々なオプションが指定可能です。上記以外のオプションはどのように指定すれば良いでしょうか?この点について解説していきます。
注意すべきオプションは "args"
・"preLaunchTask"
・"program"
になると思います。その他のオプションは "request"
に "launch"
を指定している時と同様に指定すれば良いです(デバッグ構成の名前を付けるオプション "name"
には他の構成と被らない分かりやすい名前を付けてください)。
まず、"args"
はプログラム実行時にプログラムに指定するコマンドライン引数を指定するためのオプションです。アタッチの場合、プログラムの実行は行われないため "args"
の指定は不要です。
"preLaunchTask"
に関しては、デバッグを開始する前に実行するタスクを指定するオプションです。"preLaunchTask"
に指定するタスクでビルドのみを行なっている場合、"preLaunchTask"
の指定も不要となります(既にビルドされて実行されているプログラムのプロセスに接続することになるため)。
"program"
に関しても理論上は不要です。が、どうもプラグインによっては "program"
の指定が必須のものもあるようなので注意してください。例えば C/C++ プラグインの場合は "program"
の指定が必須のようです。ですが、"program"
に指定するパスのプログラムを実行するようなことは行われないみたいで、"program"
に指定するパスに何かしらのファイルさえ存在すれば問題なくアタッチは行えます。
起動済みのプロセスをデバッグするための launch.json
ということで、ここまでの内容を踏まえれば、次のようなデバッグ構成を launch.json
に追記すれば起動済みのプロセスをデバッグ可能なデバッグ構成を用意することができることになります。
追記は "configurations": [
〜 ]
の間に行なってください。
CodeLLDB プラグイン向けのデバッグ構成
CodeLLDB プラグイン向けのデバッグ構成は下記のようになります。
{
"name": "c_lldb_attach",
"type": "lldb",
"request": "attach",
"pid": "${command:pickProcess}",
},
上記デバッグ構成を追記すれば、VSCode の「実行とデバッグ」で c_lldb_attach
という名前のデバッグ構成が選択可能になっているはずです。
C/C++ プラグイン向けのデバッグ構成
CodeLLDB プラグイン向けのデバッグ構成は下記のようになります。
ワークスペース直下に dummy
というファイルが存在することを前提としたデバッグ構成になっています。dummy
は空のファイルでも何でも良いです。
また、lldb
を使うことを想定した構成になっていますが、gdb
を使用する場合は "MIMode"
オプションの値を "lldb"
から "gdb"
に変更すれば良いはずです。
{
"name": "c_cppdbg_attach",
"type": "cppdbg",
"MIMode": "lldb",
"request": "attach",
"program": "${workspaceFolder}/dummy",
"processId": "${command:pickProcess}",
},
上記デバッグ構成を追記すれば、VSCode の「実行とデバッグ」で c_cppdbg_attach
という名前のデバッグ構成が選択可能になっているはずです。
スポンサーリンク
起動済みのプロセスをデバッグする際の注意点
一応上記のデバッグ構成を用意しておくことで、起動済みのプロセスをデバッグすることはできるようになったことになります。
実際にデバッグを行う例は、次の 起動済みのプロセスのデバッグ例 で示していきたいと思いますが、その前にいくつか注意点について説明しておきたいと思います。
プロセス ID を知る必要がある
ここまで説明してきたように、プロセスにアタッチを行なってデバッグを行うためにはアタッチ先のプロセス ID を指定する必要があります。
デバッグ構成に "processId": "${command:pickProcess}"
を指定しておけばプロセス一覧からプロセスが選択できるようにはなるのですが、プロセス一覧からアタッチしたいプロセスを探し出すのも結構大変なので、出来ることならプロセス ID を出力するようにプログラムを作ったほうが楽だと思います。
デバッグ可能なのは起動中のプロセスのみ
また、アタッチによりデバッグ可能なのはあくまでも起動中のプロセスのみになります。
すぐに終了するようなプログラムの場合、プロセス ID を選択している最中にプロセスが終了し、アタッチに失敗してしまうことになります。
ずっとプロセスが起動し続けているようなものであれば問題ないですが、プログラムがすぐに終了してしまうような場合は何らかの方法で待ち状態になるようにし、アタッチ後に待ち状態が解除されるような仕組みを導入したほうが良いと思います。
また、既に起動済みのプロセスに対してデバッグを行うわけですから、"request": "launch"
構成の時のようにプログラムの先頭からデバッグできるというわけでは無いので注意してください。あくまでもデバッグを行うことができるのはアタッチしたタイミングからになります。
スポンサーリンク
sleep
や scanf
が上手く動作しない?
先ほど挙げた『待ち状態』を作るためには sleep
や scanf
関数の利用が簡単な手段になると思うのですが、なぜか私の環境では sleep
や scanf
を利用して待ち状態を作ってもデバッグが上手く動作してくれませんでした…。
もっと具体的に現象を説明すると、sleep
や scanf
で待ち状態になっている最中にアタッチをした場合、アタッチに成功した直後に sleep
や scanf
の待ち状態が解消されるようになっていました。特に scanf
は文字列を入力しないと待ち状態が解除されないはずなのに、なぜかアタッチをすると scanf
関数が終了してしまうような状況でした。
この辺りは私の環境依存の問題になるかもしれないですが、同様の現象が発生する方もおられるかもしれませんので、一応メモ程度に注意点として挙げておきます。
ちょっと上記の現象が気持ち悪かったので、次の 起動済みのプロセスのデバッグ例 ではファイルを利用して擬似的な待ち状態を作る例を示していきたいと思います(ファイルの中身が 1
から 0
に変更された際に待ち状態を解除する)。
起動済みのプロセスのデバッグ例
ここからは、起動済みのプロセスをデバッグする方法 で示したデバッグ構成と 起動済みのプロセスをデバッグする際の注意点 で説明した注意点を踏まえ、実際のプログラムを利用して起動済みのプロセスのデバッグを行う例を示していきたいと思います。
ここでは VSCode から新たにフォルダを開いて launch.json
を作成し、その launch.json
に 起動済みのプロセスをデバッグする方法 で示したデバッグ構成を追記する手順を説明していきます
既に launch.json
を作成済みのフォルダやワークスペースが用意されている場合、そのフォルダ以下にソースコードを作成し、さらに launch.json
にデバッグ構成を追記するやり方でもデバッグを行うことが可能です
ここで紹介するソースコードでは unistd.h
や sys/types.h
をインクルードする必要があるため、Mac や Linux 環境でコンパイルして実行することを想定したものになっています。Windows 環境ではコンパイルできない可能性が大です。が、何をやっているかは参考になると思います。
通常のプログラムのプロセスにアタッチしてデバッグする
まずは、なんの変哲も無い通常のプログラムのプロセスにアタッチしてデバッグする例を示していきます。通常のプログラムなので、別に "request": "launch"
の構成でデバッグすることは可能です。
ここではアタッチによるデバッグ方法を理解していただくため、あえてアタッチを利用する方法でデバッグを行う例を示していきたいと思います。
ソースコード
下記は、通常のプログラムのソースコード例となります。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#define FLAG_FILE "flag.txt"
int getFlag(void) {
char buf[256];
FILE *fp;
int flag;
// FLAG_FILEの中身をreturn
fp = fopen(FLAG_FILE, "r");
if (fp == NULL) {
printf("fopen error");
return 0;
}
fread(buf, 1, 256, fp);
fclose(fp);
flag = atoi(buf);
return flag;
}
void initFlag(void) {
FILE *fp;
// FLAG_FILEの中身を"1"にセット
fp = fopen(FLAG_FILE, "w");
if (fp == NULL) {
printf("fopen error");
}
fprintf(fp, "1");
fclose(fp);
}
int main(void) {
// フラグを1にセット
initFlag();
printf("Process ID : %d\n", getpid());
// フラグが0になるまで待ち状態
while (getFlag()) {
sleep(1);
}
// ここからプログラムの通常処理
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
printf("%d\n", sum);
}
このソースコードにはいくつかポイントがあるので説明しておきます。
まず main
関数の先頭付近で printf
によってプロセス ID を表示するようにしています。プロセス ID は getpid
関数から取得可能です。
また、上記では flag.txt
というファイルを利用して待ち状態を作るようにしています。上記プログラムを実行すれば、プログラムを実行したフォルダに initFlag
関数によって中身が 1
のみの flag.txt
というファイルが作成されます。getFlag
関数では flag.txt
の中身を数値に変換した値を返却するようにしていますので、while (getFlag())
によって flag.txt
の中身が 0
になるまで以降の処理が実行されないようになっています。
そのため、手動で flag.txt
の中身を 0
にして保存するまでプロセスは次の処理に進まないことになります(実際には flag.txt
の中身を空にしたりしても次の処理に進むと思います)。
つまり、プログラムを実行すれば、生成されたプロセスは while
文の中の処理を延々と繰り返すことになります。flag.txt
の中身が 0
に変更されなければプロセスは終了しません。
そのため、プログラム実行後にアタッチでのデバッグを開始し、そこで printf
関数で出力されるプロセス ID を選択すれば、このプロセスに確実にアタッチすることができます。
そして、その後、手動で flag.txt
の中身を 0
にして保存してやれば、while
文を抜け出して次の行の処理に晋ことになります。
このように、アタッチするまでプログラムの処理が進まないように工夫することで、好きなタイミングから処理を再開させるようにすることができるようになります。
コンパイルとプログラムの実行
次はコンパイルを行なってプログラムファイル(実行可能ファイル)の生成を行いましょう!
デバッグを行うためには、デバッグ情報を埋め込むために -g
オプションを指定する必要があります。また、最適化を OFF するために -O0
オプションを指定することをオススメします。
例えば、上記のソースコードのファイルを main.c
とすれば、ターミナル等で下記のコマンドを実行することでデバッグ情報を埋め込んだ&最適化を OFF したプログラムファイル main
が生成されることになります。
% gcc -g -O0 main.c -o main
あとは、この main
を実行すればプロセスが生成されることになります。
% ./main
main
が実行されれば、すぐに下記のような文字列が出力されるはずです。:
の右側に表示されるのがプロセス ID であり、この ID のプロセスにアタッチすることで、上記ソースコードのプログラムのプロセスをデバッグすることができるようになります。
Process ID : 63179
デバッグ
ということで、次は先ほど生成されたプロセスにアタッチを行なってデバッグを行なっていきたいと思います。先ほど実行したプログラムは強制終了などはせずにそのまま置いておいてください。
まずは、VSCode を起動し、先ほど作成したソースコードが存在するフォルダを開きます。
さらに、VSCode で 実行とデバッグ
を選択し、さらに launch.jsonファイルを作成します
リンクをクリックします。
すると、デバッガーの選択が促されるため、自身で使用したいデバッガーを選択します。今回の例では LLDB
(CodeLLDB プラグイン) を選択します。
これにより、開いているフォルダの直下に .vscode
フォルダが作成され、その下に launch.json
が作成されます。その後、おそらく自動的に launch.json
が開かれると思います。
この launch.json
の "configurations": [
〜 ]
の部分に 起動済みのプロセスをデバッグする方法 で示したデバッグ構成を追記します。今回の例では CodeLLDB プラグインを利用するため、追記後の launch.json
は下記のようになります(コメント部分は省略しています)。
{
"version": "0.2.0",
"configurations": [
{
"name": "c_lldb_attach",
"type": "lldb",
"request": "attach",
"pid": "${command:pickProcess}",
},
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
これでデバッグ構成の準備は完了です。
次はブレークポイントの設定を行います。
まずは VSCode から先ほど作成したソースコードファイルを開き、ブレークポイントを設定します。
今回は、main
関数の中の while
文が終了した直後からステップ実行を行いたいので、int sum = 0;
の行にブレークポイントを設定します。
これでアタッチでのデバッグを行う前準備は完了したことになります。
起動済みのプロセスをデバッグする方法 で示した C/C++ プラグイン用のデバッグ構成を利用する場合は、VSCode で開いたフォルダ(ワークスペース直下)に dummy
というファイルを用意しておく必要があります
中身は何でも良いはずです
次は、いよいよデバッグを行なっていきます。
まず、実行とデバッグ
を再度開き、画面上側にあるプルダウンメニューから先ほど追加したデバッグ構成を選択します(デバッグ構成の "name"
に指定した文字列がメニューから選択できるはずです)。
選択後、メニューの左側にある再生ボタンをクリックします。
すると、下図のようなプロセス選択画面が表示されるはずです。
上側にある入力欄にプロセス ID やプロセス名を入力すればプロセスの検索が行えるようになっているので、プログラム実行時に表示されたプロセス ID をここで入力します。
アタッチするプロセスが1つだけ表示されるはずなので、ここでそのプロセスを選択します。
選択後、しばらくすると VSCode の下図のようなデバッグ用の操作バーが表示されるはずです。この状態になれば、起動済みのプロセスへのアタッチが成功したことになります。おそらく、アタッチに成功した際にはウィンドウ下側のバーの色も変わると思います。
ただし、まだ while
文が終了していない状態なのでブレークポイントには到達していないはずです。なので、まだステップ実行等の操作はできません。
while
文を終了させるため、flag.txt
の中身を 0
に変更します。この flag.txt
は上記のソースコードのプログラムを実行することにより、プログラムファイルと同じフォルダ内に自動的に生成されているはずです。このファイルを開くと 1
が入力されているはずなので、これを 0
に変更して保存します。
これにより、getFlag
関数が 0
を返却するようになって while
文が終了し、設定していたブレークポイントで処理が停止するはずです。
ブレークポイントで処理が停止すれば、あとはデバッグ操作用のバーからいつもの操作方法でステップ実行を行うことができるようになります。
アタッチを解除したい場合は、デバッグ操作バーの一番右にある 切断
ボタンを押します。これによりアタッチが解除されて処理の停止が解け、通常通りに処理を再開するようになります。このプログラムの場合は for
文が実行されてすぐに終了するはずです。
以上が、アタッチによって起動済みのプロセスをデバッグするための一連の流れとなります。
繰り返しになりますが、ポイントは、アタッチ用のデバッグ構成を launch.json
に追記すること、コンパイル時に -g
オプションを指定すること、アタッチするプロセスの ID を特定すること、さらには必要に応じて待ち状態を作ることになると思います。
最後の待ち状態に関しては必須ではなく、常に起動しているような常駐プロセスの場合は不要であったりします。逆に、すぐに処理が終了してしまうようなものの場合は待ち状態を作るようにしてアタッチするまで待たせるように工夫する必要があります。
また、今回使用したソースコードでは while
文を抜けた後に for
文で単に 0
〜 99
までの足し算を行なっていますが、ここの処理を変更することで、あなたの好きな処理のプログラムのデバッグを行うことが可能になります。
スポンサーリンク
fork
で起動したプロセスをデバッグする
先ほど紹介したプログラムに関して言えば、別にアタッチしなくても "request": "launch"
のデバッグ構成でデバッグすることは可能です。
ですが、アタッチしないとデバッグできないようなプロセスも存在します。その例の1つが fork
関数で生成される子プロセスになります。
ここまで説明してきたように、プログラムは実行するとプロセスが生成されます。
さらに、fork
関数を実行することで、そのプロセスを複製した新たなプロセスが生成されます。この新たに生成されるプロセスを子プロセスと呼びます。それに対し、プログラムを実行して生成されるプロセスは親プロセスと呼ばれます。子プロセスは親プロセスの複製であり、基本的には同じプログラムの処理が並列して実行されることになります。が、if
文等で分岐して異なる処理を並列して行わせることもできます。
デバッグ観点で重要なのは、"request": "launch"
のデバッグ構成を利用した場合、デバッグ可能なのは親プロセス側のみであるという点になります。あくまでも "request": "launch"
のデバッグ構成可能なのは、プログラム実行時に生成されたプロセスに対してのみで、それ以外から生成されるプロセスはデバッグできません。
ですが、アタッチを利用すれば、親プロセスであっても子プロセスであっても関係なくデバッグすることが可能になります。プロセス ID を適切に設定すれば、どちらのプロセスにでもアタッチすることが可能です。したがって、特に子プロセス側に対してデバッグを行いたいような場合は、ここまで説明してきたようなアタッチが有効になります。
ということで、次は fork
関数を実行する場合のデバッグ例を示していきたいと思います。
ソースコード
下記は、fork
関数を実行するプログラムのソースコードとなります。以降でデバッグを行うために、通常のプログラムのプロセスにアタッチしてデバッグする で示した main.c
と同じフォルダ内に fork.c
というファイル名で保存してください。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#define FLAG_FILE "flag.txt"
int getFlag(void) {
char buf[256];
FILE *fp;
int flag;
// FLAG_FILEの中身をreturn
fp = fopen(FLAG_FILE, "r");
if (fp == NULL) {
printf("fopen error");
return 0;
}
fread(buf, 1, 256, fp);
fclose(fp);
flag = atoi(buf);
return flag;
}
void initFlag(void) {
FILE *fp;
// FLAG_FILEの中身を"1"にセット
fp = fopen(FLAG_FILE, "w");
if (fp == NULL) {
printf("fopen error");
}
fprintf(fp, "1");
fclose(fp);
}
int main(void) {
pid_t pid;
initFlag();
pid = fork();
if (pid > 0) {
printf("Parent : %d\n", getpid());
while (getFlag()) {
sleep(1);
}
printf("Parent start!!!\n");
// これ以降で親プロセスの処理を実行する
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
printf("%d\n", sum);
exit(EXIT_SUCCESS);
} else if (pid == 0) {
printf("Child : %d\n", getpid());
while (getFlag()) {
sleep(1);
}
printf("Child start!!!\n");
// これ以降で子プロセスの処理を実行する
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
printf("%d\n", sum);
exit (EXIT_SUCCESS);
}
exit (EXIT_SUCCESS);
}
通常のプログラムのプロセスにアタッチしてデバッグする と同様に flag.txt
というファイルを利用して待ち状態にするようにしています。
fork
関数の詳細な説明は省略しますが、上記のソースコードにおいては親プロセスは if (pid > 0)
が成立した場合の処理を実行し、子プロセスは else if (pid == 0)
が成立した場合の処理を実行するようにしています。両方の場合で while (getFlag())
を実行しているため、両方のプロセスが flag.txt
の中身が 0
になるまで次の処理に進まないようになっています。
したがって、printf("Parent start!!!\n");
の行にブレークポイントを設定しておけば、親プロセス側にアタッチしている場合、flag.txt
の中身が 0
になった時にブレークポイントで停止することになります。
同様に、printf("Child start!!!\n");
の行にブレークポイントを設定しておけば、子プロセス側にアタッチしている場合、flag.txt
の中身が 0
になった時にブレークポイントで停止することになります。
コンパイルとプログラムの実行
次はコンパイルを行なってプログラムファイル(実行可能ファイル)の生成を行いましょう!
ポイントは 通常のプログラムのプロセスにアタッチしてデバッグする の時と同様なので省略します。下記のようにコンパイルを行なって実行可能ファイル fork
を生成します(前述の通り、上記で示したソースコードは 通常のプログラムのプロセスにアタッチしてデバッグする で示した main.c
と同じフォルダ内に fork.c
というファイル名で保存されていることを前提としたコマンドになっています)。
% gcc -g -O0 fork.c -o fork
続いて、この fork
を実行すればプロセスが生成されることになります。さらに、そのプロセスから fork
関数によって新たなプロセスが子プロセスとして生成されます。
% ./fork
fork
を実行すれば、すぐに下記のような文字列が出力され、親プロセスと子プロセスの ID がそれぞれ表示されるようになっています。
Parent : 63921 Child : 63923
デバッグ
続いて、アタッチを利用してデバッグを行う例を示していきます。今回は親プロセス側と子プロセス側両方にアタッチを行なってデバッグする例を示していきたいと思います。
まずは、VSCode を起動し、先ほど作成したソースコードが存在するフォルダを開きます。つまり、通常のプログラムのプロセスにアタッチしてデバッグする の時と同じフォルダを開けば良いです。そして、このフォルダにはすでに 通常のプログラムのプロセスにアタッチしてデバッグする で示した手順により launch.json
が作成されているはずなので、launch.json
関連の説明は省略します。
続いて、VSCode から先ほど作成したソースコードファイル(fork.c
)を開き、下記の二箇所にブレークポイントを設定します。
printf("Parent start!!!\n");
の行printf("Child start!!!\n");
の行
ブレークポイントを設定したら、次は 実行とデバッグ
画面の上側のプルダウンメニューからアタッチ用のデバッグ構成を選択します。そして、プルダウンメニュー左側にある再生ボタンを押します。
プロセスの選択画面が表示されるので、まずは親プロセス側の ID を指定してください。
しばらく待つと、通常のプログラムのプロセスにアタッチしてデバッグする の時と同様にデバッグ操作用のバーが表示されるはずです。これで親プロセス側にアタッチできたことになります。
続いて、再度 実行とデバッグ
画面の上側のプルダウンメニューからアタッチ用のデバッグ構成を選択し、再度プルダウンメニュー左側にある再生ボタンを押します。
プロセスの選択画面が表示されるので、次は子プロセス側の ID を指定してください。
指定すると、下の図のようなメッセージが表示されますので はい
を選択してください。
すると、子プロセスへのアタッチに成功した場合、デバッグ操作用のバーの右側にプルダウンメニューが追加されることになります。
このメニューでは、おそらく アタッチ用のデバッグ構成の名前
と アタッチ用のデバッグ構成の名前 2
の2種類が選択可能になっているはずです。
このメニューは、デバッグ対象とするプロセスを選択するためのメニューとなります。
メニューの上側の選択肢を選択した場合、デバッグ操作用のバーでは最初にアタッチしたプロセス(今回の場合は親プロセス)のデバッグを行うことができます。さらに、メニューの下側の選択肢を選択した場合、デバッグ操作用のバーでは後からアタッチしたプロセス(今回の場合は子プロセス)のデバッグを行うことができます。
ということで、この状態になれば2つのプロセスに対するデバッグを行うことができるようになったことになります。
次は、ブレークポイントで処理を停止させるために、flag.txt
を開いて中身を 0
に変更して保存してください。そうすると、設定したブレークポイントで処理が停止するはずです。
今回は親プロセスと子プロセスの2つが同じプログラムを並列して実行されることになり、さらにそれぞれのプロセスで実行される処理にブレークポイントを設定しているので、二箇所のブレークポイントで停止していることが確認できるはずです。
さらに、デバッグ操作用のバーの右側のプルダウンメニューから アタッチ用のデバッグ構成の名前
の方を選択して ステップ実行すれば、親プロセス側の処理を1行ずつ進めながらデバッグすることができます。
また、デバッグ操作用のバーの右側のプルダウンメニューから アタッチ用のデバッグ構成の名前 2
の方を選択してステップ実行すれば、子プロセス側の処理を1行ずつ進めながらデバッグすることができます。
デバッグが完了したら、デバッグ操作用のバーの 切断
ボタンを押してアタッチを解除します。この例の場合、2つのプロセスにアタッチしていますので、切断
ボタンは計2回押す必要があります。
この例のように、特にプログラム実行時以外に生成されるプロセスに対してデバッグしたい場合はアタッチによるデバッグが効果的になります。また、複数のプロセスに対してアタッチを行うことで、複数のプロセスを並行してデバッグするようなことも可能になります。
その他のデバッグ例
ここまで2つのデバッグ例を示してきましたが、アタッチでのデバッグ方法は大体理解していただけたのではないかと思います。
このアタッチでのデバッグ方法が役立つのは、デバッガーから起動できないプロセスなどに対してデバッグを行いたい場合になると思います。
fork で起動したプロセスをデバッグする で紹介した例はまさにその例で、fork
関数により起動される子プロセスはデバッガーから起動させるわけではないので、"request": "launch"
のデバッグ構成ではデバッグを行うことはできません。ですが、"request": "attach"
のデバッグ構成を利用すれば、デバッガーから起動させるプロセスでなくてもデバッグを行うことが可能となります。
また、様々な言語のプログラムが動作するような場合にもアタッチは有効です。
例えば、下記では Python スクリプトから実行される OpenCV をステップ実行する例を紹介しており、OpenCV のステップ実行を行うためにアタッチを利用しています。OpenCV は C/C++ で記述されているものを使用しています。
VSCode で Python と OpenCV を混合でデバッグ(ステップ実行)する方法!この例の場合、最初に実行するのは Python スクリプトになります。なので、デバッガーから実行するのは Python スクリプトとなります。さらに、実行対象が Python スクリプトなので、Python 用のデバッガーを利用する必要があります。そして、この際には "request"
に "launch"
を指定したデバッグ構成でデバッグを開始することになります。
そして、これにより Python スクリプトが実行され、このスクリプトから OpenCV ライブラリの提供する関数等が実行されることになります。
この場合、利用しているのが Python 用のデバッガーなので Python スクリプトはデバッグ可能です。ですが、言語が異なるため Python 用のデバッガー からは C/C++ で記述されている OpenCV はデバッグできません。
このような場合でも、Python スクリプトを実行しておき、さらにそれによって生成されるプロセスに対してC言語用のデバッガーからアタッチを行えば OpenCV 側のデバッグも行うことができるようになります。
この場合、Python 用デバッガーとC言語用デバッガーでは同一のプロセスに対してデバッグを行うことになります。が、Python で記述された処理の部分は Python 用デバッガーでデバッグを行い、C/C++ で記述された OpenCV の処理の部分はC言語用デバッガーでデバッグを行うことになり、2つのデバッガーの役割は異なることになります。
まとめ
このページでは、起動済みのプロセスをデバッグする方法について解説しました。
具体的には、起動済みのプロセスはアタッチを行うことでデバッグを行うことができ、このアタッチを行うためには launch.json
で設定するデバッグ構成の "request"
に "attach"
を指定する必要があります。
つまり、まだ起動していないプロセス・実行していないプログラムをデバッグする際には "request"
に "launch"
を指定し、既に起動しているプロセス・実行済みのプログラムをデバッグする際には "request"
に "attach"
を指定すれば良いことになります。
基本的にデバッグを行う際には "request"
に "launch"
を指定したデバッグ構成を利用する機会の方が多いと思いますが、既に起動済みのプロセスや実行中のプログラムに対してもデバッグが可能であること、"request"
に "attach"
が指定可能であることは覚えておくと良いと思います!