【VSCode】起動済みのプロセスにアタッチしてデバッグを行う(attachデバッグ構成)

このページにはプロモーションが含まれています

このページでは、VSCode で『起動済みのプロセス』に対してデバッグを行う方法について説明します!

プロセスという言葉が苦手であれば、『既に実行中のプログラム』に対するデバッグ方法と考えても良いと思います。

このサイトでは、以前に下記ページで VSCode でプログラムをデバッグする方法について解説しています。少し難しい言い方をすると、下記ページで解説している方法は、指定したプログラムを実行した際に生成されるプロセスに対してデバッグを行う方法になります。

VSCodeでMacOSにC言語デバッグ環境を構築する方法の解説ページアイキャッチ VSCodeでMacOSにC言語デバッグ環境を構築

それに対し、このページでは既に起動しているプロセスに対してデバッグを行う方法について解説を行なっていきます。

今回は、Mac 上でC言語プログラムのプロセスに対するデバッグ方法に基づいて解説をしていきますが、おそらく、考え方や設定方法に関しては他の環境や言語でも同様になると思います。

また、今回は起動済みのプロセスに対するデバッグに焦点を当てて解説していきますので、VSCode でのデバッガーの使用方法やビルドの設定等は上記ページを参考にしていただければと思います。

起動済みのプロセスをデバッグする方法

では、起動済みのプロセスをデバッグする方法について解説していきます。

ポイントは、デバッガーにプロセスを起動させるのではなく、既に起動しているプロセスへデバッガーが接続しに行くようにデバッグ構成を設定する必要がある点になります。このようにデバッガーから接続を行うことをアタッチと呼びます。

"request""attach" を指定する

下記ページでも解説しているとおり、VSCode ではデバッグの構成は launch.json というファイルで設定可能です。

VSCodeでMacOSにC言語デバッグ環境を構築する方法の解説ページアイキャッチ VSCodeでMacOSにC言語デバッグ環境を構築

例えば、CodeLLDB というプラグインを使用し、デバッガーからプログラムをデバッグする際には下記のようなデバッグ構成を用意することになります。

launch構成の例
"configurations": [
        {
            "name": "c_debug",
            "type": "lldb",
            "request": "launch",
            "program": "${fileDirname}/${fileBasenameNoExtension}",
            "args": [],
            "cwd": "${fileDirname}",
            "preLaunchTask": "c_build"
        }
    ]

上記の c_debug 構成を用いてデバッグを開始すると、"program" オプションで指定したパスのプログラムが実行されてプロセスが生成され、そのプロセスに対してステップ実行等のデバッグ操作を行うことができるようになります。

ポイントは "request" オプションの指定で、ここに "launch" を指定した場合、デバッグ開始した際にはデバッガーが "program" で指定したパスのプログラムを実行し、それによって起動したプロセスに対してデバッグが行われることになります。

launch構成でのデバッグの説明図

それに対し、起動済みのプロセスをデバッグする場合は、"request""attach" を指定する必要があります。"request" に "attach" を指定することにより、デバッガーがプログラムを実行することなく、デバッガーが直接プロセスにアタッチ(接続)しに行くようになります。

attach構成でのデバッグの説明図

上手くアタッチできれば、そのプロセスのデバッグを行うことができるようになります。

つまり、"request""launch" を指定した場合、"program" で指定したパスのプログラムが実行され、それによって生成されたプロセスに対してデバッグを行うことになります。そのため、デバッグ対象は "program" で指定したパスのプログラム実行によって生成されたプロセスに限られます。

それに対し、"request""attach" を指定した場合、プログラムの実行は行わず、既に生成済みのプロセスに対して直接アタッチしてデバッグを行うことになります。アタッチに成功するかどうかは置いておいて、アタッチ先のプロセスは自由に選択することができます。

ただし、"request""launch" を指定する時と同様に、ステップ実行等を行う場合には、デバッグ対象となるプログラムのソースコードが必要となります。

また、C言語などのコンパイル型言語の場合、デバッグ対象となるプログラムにはデバッグ情報が埋め込まれている必要があります。つまり、デバッグ対象となるプログラムをコンパイルする際には -g オプションを指定しておく必要があります(-O0 を指定しておくと最適化が OFF されてステップ実行もやりやすくなります)。

スポンサーリンク

"pid""processId""${command:pickProcess}" を指定する

前述のように "request""attach" を指定することでプロセスにアタッチすることが可能になるのですが、アタッチ先のプロセスはユーザーが指定する必要があります。

"request""launch" を指定した場合、実行するプログラムのパスを "program" に指定しておけば、そのプログラムの実行により起動するプロセスが自動的にデバッグ対象として選択されるので分かりやすいです。

では、"request""attach" を指定した場合は、アタッチ先のプロセスをどのように選択すれば良いでしょうか?

アタッチ先の指定方法の説明図1

結論としては「プロセス ID」を指定してやれば良いです。プロセスには ID (プロセス ID) が割り振らており、この ID により各プロセスを識別することができるようになっています。そのため、このプロセス ID をデバッガーに指定することでアタッチ先のプロセスを選択することが可能です。

アタッチ先の指定方法の説明図2

デバッガーにプロセス ID を指定できるようにするためには、デバッグ構成に "pid" オプションや "processId" オプションを追加する必要があります。

MEMO

利用可能なオプション名は使用するプラグインによって異なります。

例えば、CodeLLDB プラグインの場合は "pid" オプションが利用可能ですが、C/C++ プラグインの場合は "processId" オプションを利用する必要があります

これらのオプションにプロセス ID を指定してやれば、デバッグ開始時にデバッガーがその ID のプロセスにアタッチしに行くようになります。

ただ、これらのオプションには直接プロセス ID を指定するのではなく "${command:pickProcess}" を指定するのが便利だと思います。

これにより、デバッグ開始時にプロセス ID 一覧が表示されるようになり、その一覧からアタッチしたいプロセスの 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 プラグイン向けのデバッグ構成は下記のようになります。

CodeLLDB向けのattach構成
        {
            "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" に変更すれば良いはずです。

C/C++向けのattach構成
        {
            "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" 構成の時のようにプログラムの先頭からデバッグできるというわけでは無いので注意してください。あくまでもデバッグを行うことができるのはアタッチしたタイミングからになります。

スポンサーリンク

sleepscanf が上手く動作しない?

先ほど挙げた『待ち状態』を作るためには sleepscanf 関数の利用が簡単な手段になると思うのですが、なぜか私の環境では sleepscanf を利用して待ち状態を作ってもデバッグが上手く動作してくれませんでした…。

もっと具体的に現象を説明すると、sleepscanf で待ち状態になっている最中にアタッチをした場合、アタッチに成功した直後に sleepscanf の待ち状態が解消されるようになっていました。特に scanf は文字列を入力しないと待ち状態が解除されないはずなのに、なぜかアタッチをすると scanf 関数が終了してしまうような状況でした。

この辺りは私の環境依存の問題になるかもしれないですが、同様の現象が発生する方もおられるかもしれませんので、一応メモ程度に注意点として挙げておきます。

ちょっと上記の現象が気持ち悪かったので、次の 起動済みのプロセスのデバッグ例 ではファイルを利用して擬似的な待ち状態を作る例を示していきたいと思います(ファイルの中身が 1 から 0 に変更された際に待ち状態を解除する)。

起動済みのプロセスのデバッグ例

ここからは、起動済みのプロセスをデバッグする方法 で示したデバッグ構成と 起動済みのプロセスをデバッグする際の注意点 で説明した注意点を踏まえ、実際のプログラムを利用して起動済みのプロセスのデバッグを行う例を示していきたいと思います。

MEMO

ここでは VSCode から新たにフォルダを開いて launch.json を作成し、その launch.json に 起動済みのプロセスをデバッグする方法 で示したデバッグ構成を追記する手順を説明していきます

既に launch.json を作成済みのフォルダやワークスペースが用意されている場合、そのフォルダ以下にソースコードを作成し、さらに launch.json にデバッグ構成を追記するやり方でもデバッグを行うことが可能です

ここで紹介するソースコードでは unistd.hsys/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 を選択すれば、このプロセスに確実にアタッチすることができます。

アタッチが完了するまでwhileループでプログラムの処理を待機させる様子

そして、その後、手動で flag.txt の中身を 0 にして保存してやれば、while 文を抜け出して次の行の処理に晋ことになります。

アタッチ接続した後にファイルの中身を変更して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ファイルを作成します リンクをクリックします。

launch.jsonを作成する手順

すると、デバッガーの選択が促されるため、自身で使用したいデバッガーを選択します。今回の例では LLDB  (CodeLLDB プラグイン) を選択します。

デバッガーの選択画面

これにより、開いているフォルダの直下に .vscode フォルダが作成され、その下に launch.json が作成されます。その後、おそらく自動的に launch.json が開かれると思います。

この launch.json"configurations": [] の部分に 起動済みのプロセスをデバッグする方法 で示したデバッグ構成を追記します。今回の例では CodeLLDB プラグインを利用するため、追記後の launch.json は下記のようになります(コメント部分は省略しています)。

デバッグ構成追記後の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; の行にブレークポイントを設定します。

ブレークポイントを設定する様子

これでアタッチでのデバッグを行う前準備は完了したことになります。

memo

起動済みのプロセスをデバッグする方法 で示した 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 文で単に 099 までの足し算を行なっていますが、ここの処理を変更することで、あなたの好きな処理のプログラムのデバッグを行うことが可能になります。

スポンサーリンク

fork で起動したプロセスをデバッグする

先ほど紹介したプログラムに関して言えば、別にアタッチしなくても "request": "launch" のデバッグ構成でデバッグすることは可能です。

ですが、アタッチしないとデバッグできないようなプロセスも存在します。その例の1つが fork 関数で生成される子プロセスになります。

ここまで説明してきたように、プログラムは実行するとプロセスが生成されます。

プログラム実行によってプロセスが生成される様子

さらに、fork 関数を実行することで、そのプロセスを複製した新たなプロセスが生成されます。この新たに生成されるプロセスを子プロセスと呼びます。それに対し、プログラムを実行して生成されるプロセスは親プロセスと呼ばれます。子プロセスは親プロセスの複製であり、基本的には同じプログラムの処理が並列して実行されることになります。が、if 文等で分岐して異なる処理を並列して行わせることもできます。

fork関数によってプロセスが生成される様子

デバッグ観点で重要なのは、"request": "launch" のデバッグ構成を利用した場合、デバッグ可能なのは親プロセス側のみであるという点になります。あくまでも "request": "launch" のデバッグ構成可能なのは、プログラム実行時に生成されたプロセスに対してのみで、それ以外から生成されるプロセスはデバッグできません。

launch構成でデバッグ可能なプロセスを示す図

ですが、アタッチを利用すれば、親プロセスであっても子プロセスであっても関係なくデバッグすることが可能になります。プロセス ID を適切に設定すれば、どちらのプロセスにでもアタッチすることが可能です。したがって、特に子プロセス側に対してデバッグを行いたいような場合は、ここまで説明してきたようなアタッチが有効になります。

ということで、次は fork 関数を実行する場合のデバッグ例を示していきたいと思います。

ソースコード

下記は、fork 関数を実行するプログラムのソースコードとなります。以降でデバッグを行うために、通常のプログラムのプロセスにアタッチしてデバッグする で示した main.c と同じフォルダ内に fork.c というファイル名で保存してください。

fork関数を実行するプログラム
#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を指定する様子

しばらく待つと、通常のプログラムのプロセスにアタッチしてデバッグする の時と同様にデバッグ操作用のバーが表示されるはずです。これで親プロセス側にアタッチできたことになります。

続いて、再度 実行とデバッグ 画面の上側のプルダウンメニューからアタッチ用のデバッグ構成を選択し、再度プルダウンメニュー左側にある再生ボタンを押します。

プロセスの選択画面が表示されるので、次は子プロセス側の ID を指定してください。

子プロセス側のプロセスIDを指定する様子

指定すると、下の図のようなメッセージが表示されますので はい を選択してください。

別のインスタンスの起動開始の確認画面

すると、子プロセスへのアタッチに成功した場合、デバッグ操作用のバーの右側にプルダウンメニューが追加されることになります。

複数のプロセスにアタッチした際に追加されるメニュー

このメニューでは、おそらく アタッチ用のデバッグ構成の名前アタッチ用のデバッグ構成の名前 2 の2種類が選択可能になっているはずです。

追加されたメニューが、どのプロセスに対するデバッグを行うかを選択可能なメニューであることを示す図

このメニューは、デバッグ対象とするプロセスを選択するためのメニューとなります。

メニューの上側の選択肢を選択した場合、デバッグ操作用のバーでは最初にアタッチしたプロセス(今回の場合は親プロセス)のデバッグを行うことができます。さらに、メニューの下側の選択肢を選択した場合、デバッグ操作用のバーでは後からアタッチしたプロセス(今回の場合は子プロセス)のデバッグを行うことができます。

ということで、この状態になれば2つのプロセスに対するデバッグを行うことができるようになったことになります。

次は、ブレークポイントで処理を停止させるために、flag.txt を開いて中身を 0 に変更して保存してください。そうすると、設定したブレークポイントで処理が停止するはずです。

今回は親プロセスと子プロセスの2つが同じプログラムを並列して実行されることになり、さらにそれぞれのプロセスで実行される処理にブレークポイントを設定しているので、二箇所のブレークポイントで停止していることが確認できるはずです。

2つのプロセスがそれぞれブレークポイントで停止する様子

さらに、デバッグ操作用のバーの右側のプルダウンメニューから アタッチ用のデバッグ構成の名前 の方を選択して ステップ実行すれば、親プロセス側の処理を1行ずつ進めながらデバッグすることができます。

親プロセス側がステップ実行される様子

また、デバッグ操作用のバーの右側のプルダウンメニューから アタッチ用のデバッグ構成の名前 2 の方を選択してステップ実行すれば、子プロセス側の処理を1行ずつ進めながらデバッグすることができます。

子プロセス側がステップ実行される

デバッグが完了したら、デバッグ操作用のバーの 切断 ボタンを押してアタッチを解除します。この例の場合、2つのプロセスにアタッチしていますので、切断 ボタンは計2回押す必要があります。

この例のように、特にプログラム実行時以外に生成されるプロセスに対してデバッグしたい場合はアタッチによるデバッグが効果的になります。また、複数のプロセスに対してアタッチを行うことで、複数のプロセスを並行してデバッグするようなことも可能になります。

その他のデバッグ例

ここまで2つのデバッグ例を示してきましたが、アタッチでのデバッグ方法は大体理解していただけたのではないかと思います。

このアタッチでのデバッグ方法が役立つのは、デバッガーから起動できないプロセスなどに対してデバッグを行いたい場合になると思います。

fork で起動したプロセスをデバッグする で紹介した例はまさにその例で、fork 関数により起動される子プロセスはデバッガーから起動させるわけではないので、"request": "launch" のデバッグ構成ではデバッグを行うことはできません。ですが、"request": "attach" のデバッグ構成を利用すれば、デバッガーから起動させるプロセスでなくてもデバッグを行うことが可能となります。

また、様々な言語のプログラムが動作するような場合にもアタッチは有効です。

例えば、下記では Python スクリプトから実行される OpenCV をステップ実行する例を紹介しており、OpenCV のステップ実行を行うためにアタッチを利用しています。OpenCV は C/C++ で記述されているものを使用しています。

PythonとOpenCVの混合デバッグ方法解説ページのアイキャッチ VSCode で Python と OpenCV を混合でデバッグ(ステップ実行)する方法!

この例の場合、最初に実行するのは Python スクリプトになります。なので、デバッガーから実行するのは Python スクリプトとなります。さらに、実行対象が Python スクリプトなので、Python 用のデバッガーを利用する必要があります。そして、この際には "request""launch" を指定したデバッグ構成でデバッグを開始することになります。

そして、これにより Python スクリプトが実行され、このスクリプトから OpenCV ライブラリの提供する関数等が実行されることになります。

Python用のデバッガーからPythonスクリプトをデバッグする様子

この場合、利用しているのが Python 用のデバッガーなので Python スクリプトはデバッグ可能です。ですが、言語が異なるため Python 用のデバッガー からは C/C++ で記述されている OpenCV はデバッグできません。

このような場合でも、Python スクリプトを実行しておき、さらにそれによって生成されるプロセスに対してC言語用のデバッガーからアタッチを行えば OpenCV 側のデバッグも行うことができるようになります。

C言語用のデバッガーからC/C++で記述されたOpenCVをデバッグする様子

この場合、Python 用デバッガーとC言語用デバッガーでは同一のプロセスに対してデバッグを行うことになります。が、Python で記述された処理の部分は Python 用デバッガーでデバッグを行い、C/C++ で記述された OpenCV の処理の部分はC言語用デバッガーでデバッグを行うことになり、2つのデバッガーの役割は異なることになります。

まとめ

このページでは、起動済みのプロセスをデバッグする方法について解説しました。

具体的には、起動済みのプロセスはアタッチを行うことでデバッグを行うことができ、このアタッチを行うためには launch.json で設定するデバッグ構成の "request""attach" を指定する必要があります。

つまり、まだ起動していないプロセス・実行していないプログラムをデバッグする際には "request""launch" を指定し、既に起動しているプロセス・実行済みのプログラムをデバッグする際には "request""attach" を指定すれば良いことになります。

基本的にデバッグを行う際には "request""launch" を指定したデバッグ構成を利用する機会の方が多いと思いますが、既に起動済みのプロセスや実行中のプログラムに対してもデバッグが可能であること、"request""attach" が指定可能であることは覚えておくと良いと思います!

同じカテゴリのページ一覧を表示