SevenAtOneDebug

プログラミングとネットワーク、セキュリティについて書くつもりです

Behavior Treeを使って敵NPCを動かしてみる Part 2

概要

 本シリーズでは,2Dシューティングの敵NPCを動かすための意思決定アルゴリズムについて考えています.

 Part 2においては,Part 1で紹介したBehavior Treeというアルゴリズムの実装を進めていきます.Behavior Treeには制御ノード,末端ノード,修飾ノードがありますが,このプロジェクトでは,制御ノードとして,SequnceNodeとSelectorNode,末端ノードとしてActionNodeとConditionNode,修飾ノードとして,InverterNode,RepeaterNode,RetryUntilSuccessfulNode,KeepRunningUntilFailureNodeをそれぞれ実装しています.

 開発環境にはProcessingを用いています.

前回までのあらすじ

horik.hatenablog.com

 前回のパートでは,敵NPCを動かすための,キャラクタAIのアルゴリズムとして,Behavior Treeを選択しました.また,Behavior Treeのアルゴリズムの概要についても示しました.

 今回のパートでは,実際にBehavior Treeの実装を進めていきます.

開発環境

processing.org

 開発環境としては今回もProcessingを使います.ProcessingはVisual Artのためのプログラミング環境です.図形描画や画面のアップデート,キー入力の処理などが簡単に利用できて,デモ用コードを書くのに適しています.

 将来的にはUnityへの移植を念頭に置いているため,Processing内での開発言語としてはJavaを選択します.

参考実装

www.behaviortree.dev

 参考実装としては,前回に引き続き,上記のリンクの実装を据えることにします.

 ただし,本記事の内容は,参考実装を100%カバーするものではありませんので,注意してください.

Behavior Treeの実装

全体のベースとなる実装

NodeStatus
enum NodeStatus{
    SUCCESS, FAILURE, RUNNING;
}

 Behavior Treeでは,全てのノードは3種類の結果のいずれかを返します.これを列挙型としてまとめておき,コード中で使っていくことにします.ノードがSUCCESSを返す場合は,ノードが正常状態で終了したことを意味します.FAILUREの場合は失敗して終了した場合を指します.RUNNINGはノードがまだ実行状態であることを意味します.

BehaviorTreeNode
class BehaviorTreeNode{
    String name;
    NodeStatus status;

    BehaviorTreeNode(String nodeName) {
        this.name = nodeName;
    }

    /* must override this method */
    NodeStatus evalNode() {
        return null;
    }

    /* debug function */
    void printName() {
        println("the name of this node is ", name); 
    }
}

 全てノードの基本となるBehaviorNodeは以上のように書きました.BehaviorNodeはノードの名前と,ノードの実行状態の2つのフィールドを持ちます.ノードの評価はevalNode()の中に実装していきます.printName()は無くても機能上問題ないですが,ノードが自身の名前を名乗れるようにしておくと,デバッグの時に便利なので一応書いています.

制御ノードの実装

 Behavior Treeの処理の流れを制御するのが制御ノードですが,今回はSequenceNodeとSelectorNodeの2つを実装します.

 制御コードは配下に複数のノードを持ちますので,子ノードの追加と,それらの評価のメソッドを持つベースクラスを作るところから実装を始めたいと思います.

class ControlNode extends BehaviorTreeNode{
    ArrayList children;

    ControlNode(String nodeName) {
        super(nodeName);
        this.children = new ArrayList();
    }

    void addChild(BehaviorTreeNode new_node) {
        children.add(new_node);  
    }

    void printAllChildren() {
        int len = children.size();

        for (int i=0; i<len; i++) {
            children.get(i).printName();
        }
    }
}

 BehaviorTreeNodeを継承して,ControlNodeを以上のように書きました.ControlNodeは子ノードを管理するための配列を持っており,addChild()で引数として渡されたノードをこの配列に追加します.

 printAllChildren()は無くても機能上は問題ありませんが,デバッグ用途で書いています. 

SequenceNode

 Sequence Nodeの動作を上図に示しました.Sequnce Nodeでは,配下に持っている全てのノードを順番に実行していきます.この際,配下のノードは処理結果を返してきますが,これは気にせずに,次々に実行します.配下のノードを全て実行し終えたら,SUCCESSを上位ノードに返します.処理を実行している間は常にRUNNINGを返します.

class SequenceNode extends ControlNode {
    int numberChildren         = 0;
    int numberExecutedChildren = 0;

    SequenceNode(String nodeName) {
        super(nodeName);
    }

    NodeStatus executeAllChildren() {
        BehaviorTreeNode processNode;
        NodeStatus sequenceStatus = null;
        NodeStatus processResult;

        numberChildren = this.children.size();
        if (numberExecutedChildren == numberChildren) {
            return NodeStatus.SUCCESS;
        }

        // check the child node currently being processed
        processNode = children.get(numberExecutedChildren);

        // decide sequence status depend on the result of the current node.
        processResult = processNode.evalNode();
        switch (processResult) {
            case SUCCESS:
                numberExecutedChildren++;
                sequenceStatus = NodeStatus.RUNNING;
                break;
            case FAILURE:
                numberExecutedChildren++;
                sequenceStatus = NodeStatus.RUNNING;
                break;
            case RUNNING:
                sequenceStatus = NodeStatus.RUNNING;
                break;
        }
        return sequenceStatus;
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result;
        result = this.executeAllChildren();
        return result;
    }
}

 ContolNodeを継承し,以上のようにして,SequenceNodeをつくりました.evalNode()をoverrideして,executeAllChildren()の実行結果をこのノードの処理結果として返すようにしています.executeAllChildrenでは,childrenに追加された子ノードを順番に実行していきます.

 Part1でも紹介したように,Behavior Treeにおいては,Tick()によって何度もツリーを辿ることによって処理が進んでいきます.SequenceNodeはその中で,配下のどのノードを実行しているのかを管理する必要があります.そのために,numberExecutedChildrenという実行されたノードの個数を保持するフィールドを持っています.この値を使って,executeAllChildren()の中で,その回で実行すべき子ノードをchildrenから引っ張り出します.子ノードの個数はnumberChildrenというフィールドに保持されており,numberExecutedChildrenとnumberChildrenが同じ値であれば,配下のノードは全て実行されたということですので,SUCESSを返します.そうでないなら,引っ張り出した子ノードを実際に実行して,その結果に応じた処理をします.

 子ノードの実行結果についてですが,SequenceNodeでは,子ノードの結果は気にしませんので,処理中は常にRUNNINGを返すようにしています.

SelectorNode

 SelectorNodeの動作を上図に示しました.SelectorNodeは,適当なアクションが取れるところまで処理を続ける制御ノードです.配下のノードを順に処理していく部分についてはSequenceNodeと同様ですが,配下のノードの処理結果を加味して動作します.

 配下のノードがSUCCESSを返してきた場合には,何らかの意味のある動作がとれたということですので,上位ノードにSUCCESSを返して処理を終了します.このとき,SUCCESSを返したノード以降のノードは実行されないということに注意してください.SUCCESSを返すようなノードがないときは,次々とノードが実行されていきますが,全てのノードにおいてFAILUREが返された場合には,意味のある行動をとることに失敗したということですので,Selector Node自体も上位ノードにFAILUREを返して終了します.

class SelectorNode extends ControlNode {
    int numberChildren         = 0;
    int numberExecutedChildren = 0;

    SelectorNode (String nodeName) {
        super(nodeName);
    }

    NodeStatus executeChildren() {
        BehaviorTreeNode processNode;
        NodeStatus selectorStatus = null;
        NodeStatus processResult;

        numberChildren = this.children.size();
        if (numberExecutedChildren == numberChildren) {
            return NodeStatus.FAILURE;
        }

        // check the child node currently being processed
        processNode = children.get(numberExecutedChildren);

        // decide selector status depend on the result of the current node.
        processResult = processNode.evalNode();
        switch (processResult) {
            case SUCCESS:
                selectorStatus = NodeStatus.SUCCESS;
                break;
            case FAILURE:
                numberExecutedChildren++;
                selectorStatus = NodeStatus.RUNNING;
                break;
            case RUNNING:
                selectorStatus = NodeStatus.RUNNING;
                break;
        }
        return selectorStatus;
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result;
        result = executeChildren();
        return result;
    }
}

 ControlNodeを継承して,上のようにSelectorNodeを書きました.evalNode()をoverrideして,executeChildre()で配下のノードを実行した結果をSelectorNodeの結果として返すようにしています.

 executeChildren()では,SequenceNodeと同じ要領で,配下の子ノードを順番に実行しています.違っているのはノードを全て実行し終えた時の処理と,子ノードの結果の扱いです.

 SelectorNodeでは,子ノードを全て実行し終えるということは,適切なアクションが取れなかったということを意味します.このため,numberChildrenとnumberExecutedChildrenの値が一致した場合では,SequenceNodeとは反対にFAILUREを返すようにしています.

 また,子ノードの処理結果の扱いに関しては,SequenceNodeでは子ノードの処理結果がSUCCESSであってもFAILUREであってもnumberExecutedChildrenをインクリメントして,次のノードに処理を進めていましたが,SelectorNodeではFAILUREの時だけ処理を次ノードに進めます.これによって,SUCESSを返す子ノードがあった場合にはそのノードに留まるので,以降のノードが実行されなくなります.SUCCESSを示すノードがあったときは,SelectorNode自体もSUCCESSを返すことになります.

末端ノードの実装

 末端ノードは,ゲーム中でのキャラクタの挙動に対応するノードです.大まかには行動に対応するActionNodeと意思決定に対応するConditionNodeがあります.

 ここで,末端ノードはキャラクタのアクションを作るわけですが,ゲーム上でのキャラクタのアクションというのは,ゲームやそのキャラクタに対して固有のものです.つまり,それ専用のコードがBehavior Treeのコードとは別にあるわけです.そのため,Behavior Tree上で実際に使う末端ノードは,Behavior Treeに関する処理と,キャラクタの挙動を作るコードを合わせたものということになります.

 こういうわけで,どこでも使える一般的な末端ノードを作ることは難しいです.そこで,アクションに与えられた残り時間の管理や,上位ノードへの結果の応答といった,Behavior Tree上での末端ノードの挙動だけを実装したActionNodeクラスを実装し,これを継承する形で,ゲームで使う末端ノード作ることにします.

 なお,図上ではActionNodeのみ記載していますが,ConditionNodeについても同じ要領です.

 Part2 (本パート)では,ベースとなるActionNodeとConditionNodeをそれぞれ実装します.これらをPart4で,ゲームで使う末端ノードに仕立てる予定です.

LeafNode
/* This is a base class for leaf nodes. */
class LeafNode extends BehaviorTreeNode{ 
    NodeStatus status;

    LeafNode(String nodeName){
        super(nodeName);
    }
}

 末端ノードのベースとなるクラスを上のように書きました.中身は非常にシンプルで,持っているフィールドは処理結果を示すstatusだけです.

 このクラスを作らずに,直接BehaviorTreeNodeを継承させることもできるのですが,末端ノードは末端ノードで,それ用のベースクラスから作った方が理解しやすい気がしたので使っています.

ActionNode

 ActionNodeは,キャラクタの行動を作る末端ノードのベースとなるクラスです.Behavior Treeにキャラクタの動作のスクリプトを紐づける役割をしています.このクラスが達成すべきことは次の2つです.

 1つ目は,キャラクタの行動に与えられた残り時間を管理することです.キャラクタが行動に使える時間は限られているのが普通ですので,残り時間を常に管理する必要があります.これによって,時間切れとなった時に,失敗とみなして次のタスクに移ったり,想定した時間よりも早く動作が完了した時に動作を繰り返したりすることが可能になります.

 2つ目は,上位ノードに対して行動の結果を通知することです.ActionNodeはキャラクタの行動に紐づくものですから,そのキャラクタがとった行動が成功だったのか,失敗だったのか,あるいはまさに今取り組んでいる途中なのか,を上位ノードに知らせなければなりません.

class ActionNode extends LeafNode{
    boolean isFinished      = false;
    boolean enableRepeat    = false;  // by default, repeating is disabled. 
    int requiredTotalFrames = 0;
    int remainedFrames      = 0;      // it's decremented every Action() calls

    ActionNode(String nodeName, int requiredTotalFrames){
        super(nodeName);
        this.requiredTotalFrames = requiredTotalFrames;
        this.remainedFrames      = requiredTotalFrames;
    }

    NodeStatus Action(){
        if(0 < remainedFrames){
            remainedFrames--;
            return NodeStatus.RUNNING;

        /* execute process to finite the action if remaindFrames reaches 0 */
        }else{

            /* reset */
            if(enableRepeat){
                isFinished = false;
                remainedFrames = requiredTotalFrames;
            }
            return NodeStatus.SUCCESS;
        }
    }

    @Override 
    NodeStatus evalNode(){
        NodeStatus result;
        result = this.Action();
        return result;
    }
}

 Action Nodeを上記のように実装しました.overrideしたevalNode()が呼ばれることで,Action()が実行された結果が返されます.

 Action()は前述した時間管理と,処理結果の決定に対応しているメソッドです.時間管理にはrequiredTotalFramesとremaindFramesの二つのフィールドを使います.requiredTotalFramesはこのクラスが作られる時に設定される値で,このアクションに対して割り当てられた時間(フレーム数)を表します.それに対して,remaindFramesは今残っている時間(フレーム数)を表します.remaindFramesは始めはrequiredTotalFramesで初期化されますが,実行されるたびに1ずつ減っていき,やがて0になります.remaindFramesの値が残っている間は,処理結果としてRUNNINGを返し続けて,0を下回ったときは処理が終了したということですのでSUCCESSを返します.

 また,このAction Nodeは処理の繰り返しができるようになっており,そのためにenableRepeatというフィールドを持っています.このフィールドをtrueに設定しておくと,remaindFramesが0になって処理が終わった時に,remaindFramesがリセットされます.これによって処理が繰り返されます.

 処理が終了したかどうかの判定にisFinishというフィールドを定義しています.これは後々,このクラスを拡張してキャラクタの専用の末端ノードを作る時に使っていくことになります.

ConditionNode

 ConditionNodeはキャラクタの意思決定に対応するノードです.ゲーム上であれば,目標との距離が規定の距離よりも近いか,必殺技が使用可能か,HPが規定以上残っているか,鍵が空いているか,など,キャラクタを取り巻く状況を評価して,その後の行動の決定に使います.

 ターン制のRPGボードゲームは別として,アクションゲームではキャラクタの思考のために特別に時間を割くことはないので,ConditionNodeでは1回のTick()で処理結果を返します.このため,処理結果はSUCCESSかFAILUREのどちらかで,RUNNINGを返すことはありません.

class ConditionNode extends LeafNode{
    boolean isMet = false;
    NodeStatus status;

    ConditionNode(String nodeName){
        super(nodeName);
    }

    void checkCondition(){
        if(isMet){
            status =  NodeStatus.SUCCESS;
        }else{
            status =  NodeStatus.FAILURE;
        }
    }

    @Override
    NodeStatus evalNode(){
        checkCondition();
        return this.status; 
    }
}

 ConditionNodeを上のように実装しました.overrideしたevalNode()でcheckConditionが呼び出されて,これによって処理結果がセットされ,これが応答されます.非常にシンプルです.

 checkCondition()は,条件が満たされているかどうかを判定するisMetというフィールドを元に処理結果をセットします.

 条件が満たされているのかの判定については,ゲーム固有,キャラクタ固有のコードになりますので,ConditionNodeを継承してキャラクタ固有の条件判定用ノードを作る際に記述していきます.

修飾ノードの実装

 修飾ノードは配下のノードの動作に変更を加えるためのノードです.指定回数の繰り返しや,処理結果の反転などが代表的です.

 様々な種類の修飾ノードが考えられますが,このプロジェクトでは,InverterNode,RepeaterNode,RetryUntilSuccessfulNode,KeepRunningUntilFailureNodeを実装します.

 なお,配下のノードの個数についてですが,修飾ノードでは基本的に1つしか子ノードを持たないということに注意してください.複数のノードを修飾の対象としたいときは,制御ノードを間に噛ませると良いです.

DecoratorNode

 制御ノードや末端ノードの場合と同様に,ベースクラスを作るところから始めていきます.

/* This is a base class for decorator nodes. */
class DecoratorNode extends BehaviorTreeNode {
    BehaviorTreeNode child;

    DecoratorNode(String nodeName) {
        super(nodeName);
    }

    void setChild(BehaviorTreeNode new_node) {
        this.child = new_node;
    }
}

 修飾ノードのベースクラスであるDecoratorNodeを上のように実装しました.DecoratorNodeは子ノードchildを持ち,これを修飾の対象とします.childを設定するために,setChild()というメソッドを定義しました.これによって,外部から引数として渡されたノード子ノードに設定できます.

InverterNode

 InverterNodeは,配下のノードの処理結果を反転する修飾ノードです.

/* InverterNode is a node to invert the result of child node and return it */
class InverterNode extends DecoratorNode {

    InverterNode(String nodeName) {
        super(nodeName);
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result;
        result = child.evalNode();

        switch(result) {
          case SUCCESS:
            result = NodeStatus.FAILURE; 
            break;
          case FAILURE:
            result = NodeStatus.SUCCESS;
            break;
          case RUNNING:
            result = NodeStatus.RUNNING;
            break;
        }
        return result;
    }
}

 InverterNodeを上のように実装しました.overrideしたevalNode()で,結果の反転をしています.配下ノードのevalNode()を呼び出して得た結果がSUCCESSであった場合にはFAILUREを返し,FAILUREであった場合にはSUCCESSを返します.

 RUNNINGの場合は,まだ配下のノードが実行中ですのでRUNNINGを返します.

RepeaterNode

 RepaterNodeは,配下のノードを指定の回数だけ繰り返し実行する修飾ノードです.

/* RepeaterNode is a node to execute the specific child node repeatedly */
class RepeaterNode extends DecoratorNode {
    int repeatCount = 0;

    RepeaterNode(String nodeName, int repeatCount) {
        super(nodeName);
        this.repeatCount = repeatCount;
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result = child.evalNode();

        // it's termination condition of this node.
        if (repeatCount == 0) {
            return NodeStatus.SUCCESS;
        }

        switch (result) {
            case SUCCESS:
                result = NodeStatus.RUNNING;
                repeatCount--;
                break;
            case FAILURE:
                result = NodeStatus.FAILURE;
                break;
            case RUNNING:
                result = NodeStatus.RUNNING;
                break;
        }
        return result;
    }
}

 RepeaterNodeを上のように実装しました.繰り返し回数を示すrepeatCountというフィールドを持っています.evalNodeでは,配下のノードが成功するたびにこのカウンタをデクリメントしていき,最終的に0になった時にSUCCESSを上位ノードに返します.

 現状の実装では配下ノードがFAILUREとなった時に,ループから抜けられなくなるという問題があります.僕のデモでは特に問題にならなかったので,放置されているのですが,デモ以外の用途でこうしたコードを使うときは,もうすこし防衛的に書くべきです.

RetryUntilSuccessfulNode

 RetryUntilSucessfulNodeは,配下のノードが成功するまで繰り返し実行する修飾ノードです.数回は失敗する前提のアクションを取るときなどに使用します.ただ,試行回数の上限は決めておく必要があって,そうしないと,成功の見込みがない状態で試行を始めた時に,そのアクションから抜け出せなくなってしまいます.

class RetryUntilSuccessfulNode extends DecoratorNode {
    int numberAttempt;

    RetryUntilSuccessfulNode(String nodeName, int numberAttempt) {
        super(nodeName);
        this.numberAttempt =  numberAttempt; 
    }

    @Override 
    NodeStatus evalNode() {
        NodeStatus result;

        result = child.evalNode();
        switch (result) {
            case SUCCESS:
                result = NodeStatus.SUCCESS;
                break;
            case FAILURE:
                result = NodeStatus.RUNNING;
                numberAttempt--;
                break;
            case RUNNING:
                result = NodeStatus.RUNNING;
                break;
        }

        if (numberAttempt == 0) {
            result = NodeStatus.FAILURE;
        }
        return result;
    }
}

 上のようなコードでRetryUntilSuccessfulNodeを実装しました.試行できる残り回数をnumberAttemptというフィールドに保持していて,下位ノードの結果がFAILUREであると,これがデクリメントされて,再度試行されます.numberAttemptが0になってしまったら,もうこれ以上は試行できないので,上位ノードにFAILUREを返します.

 numberAttepmtが0になるまでは,失敗でも試行を続けますので,下位ノードがFAILUREを返した場合でも上位ノードにはRUNNINGを返します.

KeepRunningUntilFailureNode

 KeepRunningUntilFailureNodeは,配下のノードが失敗するまで繰り返す修飾ノードです.RetryUntilSuccessfulNodeを反対にしたようなノードなのですが,回数の上限を特に設けていないという部分が異なります.下位ノードがSUCCESSを返している状態は望ましい状態であると言えるので,名前にKeepと入っていることから分かるように,できる限りその状況を維持しようとします.

 ゲームの文脈では,炭鉱夫がひたすら金脈を掘り続けるみたいな状況で使い道があるかと思います.

class KeepRunningUntilFailureNode extends DecoratorNode{

    KeepRunningUntilFailureNode(String nodeName) {
        super(nodeName);
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result;

        result = child.evalNode();
        switch (result) {
          case SUCCESS:
            result = NodeStatus.RUNNING;
            break;
          case FAILURE:
            result = NodeStatus.FAILURE;
            break;
          case RUNNING:
            result = NodeStatus.RUNNING;
            break;
        }
        return result;
    }
}

 KeepRunningUntilFailureNodeを上のように実装しました.非常にシンプルで,下位ノードがFAILUREを返した場合以外では,上位ノードにRUNNINGを返し続けます.

クラスの継承関係の確認

 このパートでは,Behevior Treeを実装するために様々なクラスを作成しましたので,ここで一旦,クラス同士の関係を整理しておきたいと思います.

 クラスの継承関係を上図に示しました.全てのベースになっているのはBehaviorTreeNodeです.これを継承して,LeafNode,ControlNode,DecoratorNodeが作られました.それぞれが末端ノード,制御ノード,修飾ノードに対応しています.

 LeafNodeを継承して,キャラクタの行動に対応するActionNodeと,意思決定に対応するConditionNodeが作られました.

 ControlNodeを継承して,処理の順次実行をするSequenceNodeと,選択的な実行をするSelectorNodeが作られました.

 DecoratorNodeを継承して,処理の反転をするInverterNode,指定回数を繰り返すRepeaterNode,成功まで繰り返すRetryUntilSucessfulNode,失敗するまで繰り返すKeepRunningUntilFailureNodeが作られました.

まとめ

 Processingをつかって,Behavior Treeで必要なノード群を実装しました.

 制御ノードとして,SequnceNodeとSelectorNodeをそれぞれ実装しました.

 末端ノードとしてActionNodeとConditionNodeをそれぞれ実装しました.

 修飾ノードとして,InverterNode,RepeaterNode,RetryUntilSuccessfulNode,KeepRunningUntilFailureNodeをそれぞれ実装しました.

次回やること

 このシリーズではゲーム上の敵NPCをBehavior Treeを使って動かすことを目的としています.Part 1,Part 2ではBehavior Treeの実装を進めてきましたが,このアルゴリズムを適用するためのデモ用ゲームがまだ準備されていません.

 そこで,次回のPart 3では,デモ用として,簡単な2Dシューティグゲームを実装します.以前の弾幕シューティングの記事でも似たようなことを実はしているのですが,今回は,HPとダメージ計算も導入して,きちんと勝敗がつくようにします.

別パートへのリンク

Part 1 : 

horik.hatenablog.com

Part 2 :

現在地点.この記事です.

Part 3 :

工事中

Part 4 :

工事中

コード全体

 今回紹介したコードの全体は以下で確認できます.

github.com

可読性に関する本を読んだ -プログラミング作法,他2冊-

概要

 プログラムを書く機会が増えてきたので,プログラミング作法をはじめとして,コードの可読性に関する本をいくつか読みました.この記事では,それらの本を振りかえって,次に学ぶ内容などを検討します.

経緯

 この頃,プログラムを書く機会が非常に増えてきてました.それだけでなく,書くプログラムの規模もだんだんと大きくなってきています.小規模なプログラムであれば,見通しを確保するのは容易ですし,デバッグやテストも簡単なのですが,プログラムの規模が増大すると,こうしたことは途端に困難になります.

 そういうわけで,プログラムの可読性が非常に重要視されるわけですが,これまで,このトピックについてあまり真剣に学んでこなかったという部分があるので,今年度の前期の期間を使って,いくつか書籍を読んで勉強していました.この記事では,それらについて簡単に振り返ろうと思います.

読んだ本紹介

プログラミング作法

www.kadokawa.co.jp

 基本的には可読性の高いプログラムを書くことを目的とする本ですが,コードの良し悪しを検討するための様々な観点を与えてくれる本でした.

 表層的な改善については第1章にコンパクトにまとまっていて,慣習的な命名やシンプルな制御構造を使う重要性を説いています.関数などのインタフェースについては,4章で詳しく説明されます.

 この本は特にデータ構造についての説明が手厚いです.基本となる4つのデータ構造(配列,リスト,ツリー,ハッシュ)の性質が詳しく整理されています.そして,実際にプログラムが扱うデータに対して,どのようにデータ構造を選択するかというのを実例付きで説明しています.この辺りの説明のおかげで,データ構造の選択を,これまでよりもはるかに身近な問題として考えられるようになりました.

 本の中盤以降では,テストやデバッグ,パフォーマンスチューニングなどの,プログラミングの周辺の分野についても詳細に説明されていました.それぞれの項目では,基本スタンスがきちんと示された上で,発展的な手法が紹介されているので,実践もしやすいかと思います.

 最後には移植性や,課題分野において適切なプログラミング環境をどのように構築するのかというような話題にも触れられていました.これらの話題は,コードの可読性とは少し違う部分で,コードの特性を判断する上で役立つと思いました.

リーダブルコード

www.oreilly.co.jp

 あまりに有名な本ですが,コードの可読性に関して非常に記述が強い本です.本は全体として,表層的な改善と,根本的な改善に分かれています.

 表層的な改善の部分では,変数や関数に対する命名のほか,コードの配置,コメントの付け方などが指南されています.コメントについては,いわゆる教科書的な"Whyを書く"のとは違っていて,コードのヒントになるようなコメントは積極的につけることを提案しています.この辺りがプログラマの認知負荷を下げるという目的に,正面から向き合っている感じがします.

 根本的な改善の部分では,データ構造にまで手を入れて改善をかけたりしているのですが,改善が段階的に進むような構成で書かれていて,ライブ感がありました.これによって,ヘルパー関数を定義したり,処理をまとめたりといった,操作がどのタイミングでなされているのかが追えるようになっています.コードを書くとき,それぞれの局面で何を考えるべきか,についての非常に良い教材であると思います.

 文体はかなり易しめで読みやすいです.分量もコンパクトなので,読み返すのも容易かと思います.

良いコードを書く技術

gihyo.jp

 コードの可読性に関しての議論が非常にコンパクトにまとまっている本です.

 方針をきちんと説明するというよりは,コード例をたくさん示すことで,雰囲気を掴んでいくという印象でした.

 文体は優しいのですが,最終的な判断基準のところに関しては,経験を積むことで掴めてくる,というような説明が多用されていて,踏み込みが甘い印象です.上2冊がよめれば,特段これを読む必要がないかと思います.あくまで取っ掛かりとして読む用という感じでした.

次のステップ

 どの本でも共通して,テストの重要性について説いていましたが,これまではセオリー通りのテストというのが実施できていませんでした.これからは反省して,テスト手法のセオリーを学び,堅牢なテストが実施していきたいところです.さしあたっては,"単体テストの考え方/使い方"という本を読み進めています.テストの基本的なアイデアからきちんと説明されていて,今のところいい感じです.

book.mynavi.jp

 それから,コードの可読性の次は,システム全体についての広い視点を持てるようになりたいです.システム全体がうまく構成できれば,それぞれのコンポーネントの役割は明瞭になりますので,それらのコードも簡潔に書ける可能性が高まります.そのために,今後は,ソフトウェアアーキテクチャデザインパターンについても手を出していきたいです.手元に"ソフトウェアアーキテクチャの基礎"という本を積んでいるので,これを崩すところからでしょうか.

www.oreilly.co.jp

まとめ

プログラミング作法,リーダブルコード,良いコードを書く技術の3冊を読みました.それぞれの本から,コードの改善のテクニックが学べました.

今後は,テスト手法やソフトウェアアーキテクチャについても学びを深めていきたいです.

OWASP Juice Shopをビルドする

概要

 この記事ではOWASPが提供しているやられサイトであるJuice shopを導入方法を説明しています.今回はローカル環境へのインストールをソースコードからやる方法をとりました.x64のarch linuxを利用しました.

目的

 WAFの検証をするために,防御対象のWebサイトが必要になりました.目的を考えると,脆弱に作られたやられサイトの方が反応がわかりやすくて,検証に使いやすそうです.そこで,OWASP Juice shopという,OWASPが提供しているやられサイトを使うことにしました.

作業の流れ

pwning.owasp-juice.shop

 Juice shopの導入については,以上のページの内容に従ってやっていきます.今回はローカル環境へのインストールで,ソースコードからビルドしますので,以下のような流れで作業します.

 なお,作業環境はx64のArch Linuxです.debian系のディストリビューションを使用している場合は,パッケージマネージャーの部分の説明をaptで読み替えてください.

  1. nodejsをインストールする
  2. githubからコードを取ってくる
  3. npm install
  4. npm start
  5. 動作確認

実作業

nodejsをインストールする

pacmanでnodejsとnpmをインストールします.

sudo pacman -S nodejs
sudo pacman -S npm    

githubからコードを取ってくる

github.com

 上のリンクからコードを任意のディレクトリにクローンします.

git clone https://github.com/juice-shop/juice-shop.git

npm install

 クローンしたディレクトリに移動して,npm installを実行します.

cd juice-shop/
npm install

npm startして失敗した

 インストールが終わったら,npm startでスタートします.

npm start

 しかし,ここで失敗してしまったようです.pacmanでnodejsをインストールしようとすると最新版の24.6.0-1が入るのですが,Juice shopはv20~22の間のnodejsを要求しているようです.そこで,nodejsをdowngradeすることにします.

 調べてみると,nvmというnodejsのバージョンを管理するシステムがあり,これを使うことで簡単にバージョンを変えられるようです.

v22のnodejsを手にいれる

 まずは,nvmを導入します.

sudo pacman -S nvm

 nvmを有効にするには,さらに,.bashrcに次の一文を追加します.

source /usr/share/nvm/init-nvm.sh

 そして,bashをリロードして,nodejsのv22をインストールします.

source ~/.bashrc
nvm install 22

rebuildしてスタートする

 nodejsのversion 22が手に入ったので,これで先ほど一度導入してしまったjuiceを再度ビルドし直します.ビルドが終わったら,juice shopを起動します.

npm rebuild
npm start

動作確認

 Juice Shopを起動すると,デフォルトではlocalhostの3000番ポートで待ち受けますので,ここにブラウザでアクセスします.

 アクセスできました.これにて導入作業は完了です.

まとめ

 WAF検証のために,OWASP Juice Shopをソースコードから導入しました.

 Juice shopはnodejsのv20~v22を要求しており,nvmを使うことで,適切なnodejsを導入できました.

 

宅内LAN環境の整備 : BMAXの一万円のミニPCを導入

概要

 この記事では,開発作業用のLinuxマシン(実機)を作業用のセグメントに導入する流れを説明しています.

 BMAX社の,一万円程度で購入できる安価なミニPCを購入し,このマシンのOSをArch Linuxに載せ替えて作業用セグメントに追加することで,目的を達成しました.

前回までのあらすじ

 前回までの整備によって,作業用のセグメントのWindowsマシンをRDPで快適に操作できるようになりました.

horik.hatenablog.com

今回やること

 作業用セグメントでWindowsMacの環境がそれぞれ使えるようになりました.ただ,開発作業やCTFでは,x64のLinux環境が欲しくなることがあります.そこでx64のLinuxマシンを作業用セグメントに導入したいです.

別の選択肢について

 実機を導入する以外の手段としては,WSLが有望な選択肢です.WindowsのWSL環境も今日は非常によくなってきています.ただ,通常のLinux環境とは異なる挙動をすることがしばしばある点が厄介です.そして,そもそも,Linuxを使うためにいちいちWindowsマシンに接続するのは面倒です.

 そのほかには,Appleシリコンのmacにおいてもx64のLinuxマシンをエミュレートすることもできます.しかし,実際に使ってみたところ,パフォーマンスが悪くて,快適に開発できる環境ではありませんでした.

 AWSなどのクラウド環境やXserverのVPSなどを利用していた時期もありましたが,なんだかんだでランニングコストがかかってしまいます.長期利用を考えると,実機を導入してしまった方が安上がりだと考えます.

BMAX MaxMini B1 mini

 Linuxマシンとして使うために,BMAXのMax Mini B1 miniというパソコンを買いました.話題の中華メーカーの格安ミニPCの一つで,Amazonなどでは12000円くらいで購入できます.調べてみて気づいたのですが,現在,ミニPC市場は格安PCの主戦場になっているようで,安価な製品が大量に出回っています.その中でも特に価格面を押し出して戦っているのが,中華のGMKTekとBMAXという印象です.今回はBMAXのマシンの方が安かったので,そっちに手を出しました.

jp.bmaxit.com

 届きました.割合きちんとした箱で届いて,テンションが上がりますね.

 マシンの外観についてですが,とにかく小さくて軽いです.サイズは折り畳みの財布と大体同じくらいで,重量も200gしかないからマグカップよりも軽いですね.ロゴデザインがあんまりかっこよくないのは,この際よしとしましょう.

 電源ボタンは前面にあって,押しやすいのでいいですね.

 マシンスペックについては以下のようになっています.(HPから抜粋)

CPU:Intel®N4000
GPUIntel®UHDグラフィックス
メモリ:8GB
ストレージ:128GB
システム:Windows 11
WiFi:2.4GHz/5GHzデュアルバンドWi-Fi
BluetoothBluetooth 5.0
インターフェイス:USB×4、DC×1、ヘッドセットジャック、HDMI×2、RJ45×1
拡張可能なストレージ:M.2 2280 SSD SLOTX1
外観:210gのスリムで軽量

 N4000は2コア2スレッドのCPUで,定格クロックが1.1GHz,ブースト時で2.6GHzです.VPSサービスの一番安いサーバーくらいの性能です.性能は低いですが,僕はプログラミングくらいしかやらないし,作っているのも組み込み向けのシステムなので,そこまで問題にはならないかなという感じです.Windows11を走らせてメインマシンとしてガンガン使うというようなものではない気がしますが,他の人のレビューを読んでみると,ブラウジングやOfficeの軽作業くらいならこなせるようです.

 RAMは8GB載っていて,これは十分な量かなと思います.Strageも128GBあって,個人的には十分な容量と感じます.

 ネットワークインタフェースが,RJ-45とだけ書いてあってスピードがわからないのですが,調べてみたら,きちんと1GBps出せるデバイスでした.無線のインタフェースもきちんと動作しているようですが,僕の環境では使いません.

Arch Linuxへの移行

 このマシンにはWindows11が標準でインストールされていますが,これをLinuxに置き換えます.利用するディストリビューションですが,今回はミニマルさを重視して,Arch Linuxを使うことにします.

 Archのインストールの細かい手順については,この記事では説明しませんが,Arch Wikiのインストールガイドを見ながら進めていきます.

wiki.archlinux.jp

 大まかには次のような作業手順でした.

1. パーティションを切る ( UEFI用,swap用,システム用で3つ作成した)

2. ファイルシステムを作成する

3. 作成したファイルシステムをマウントする

4. ネットワーク設定をする

5. Arch Linux本体をインストール

6. システム側に導入したArch Linuxにログインする

7. Grubなどを導入する.また必要な設定(ネットワーク設定など)を実施

8. Userなどの設定をする

9. 再起動.

宅内ネットワークへの導入

 準備したLinuxマシンにアドレスを振って,作業用セグメントに導入します.

 画像のように,10.0.0.4を振って,作業用ネットワークセグメントに追加しました.さらに,sshdを立ち上げて,mac miniからsshで接続ができるように設定します.

 接続できました.以上で,今回の作業は完了です.

使用感

 Linuxマシンを導入してから1ヶ月ほど利用していますが.今のところ快調に動作しています.やはり,素のLinuxmacから気楽に接続して使えるのは非常に便利です.

まとめ

 BMAXのMaxMini B1 miniを導入しました.マシンのOSとしては,ArchLinuxをインストールしました.

 作業用セグメントに導入したマシンを追加して,メインマシンのMac Miniからアクセスできるようにしました.

Behavior Treeを使って敵NPCを動かしてみる Part 1

概要

 2Dシューティングゲーム上で敵NPCを動かすための,意思決定アルゴリズムについて考えます.本シリーズでは意思決定アルゴリズムとしてBehavior Treeを採用することとし,全4回に渡って,その実装について考えます.最終的にはProcessingによって,簡単な対戦デモを実装する予定です.

 Part1では,Behavior Treeのアルゴリズムの概要を見ていくことにします. 

経緯

 友人(@Ririn_main)と一緒に,2Dシューティングゲームの開発を進めています.当ブログでは以前,シューティングゲームで用いる弾幕要素について考えました.

horik.hatenablog.com

 見映えがして,かつ,避けにくい弾幕を作ることによって,プレイヤは回避行動に手応えを感じて,ゲームを楽しむことができます.

 しかし,プログラムをゲームとして成立させるためにはこれだけでは不十分です.アクションゲームはあくまでも敵キャラクタと戦うものですから,敵キャラクタのふるまいの実装が欠かせません.

 ベタ書きで実装を書いていくことも可能ではありますが,ゲームには複数のキャラクタが登場するため,複数キャラ間で共通して使いまわせて,かつ高い柔軟性が達成できる枠組みを準備するべきです.

 そこで,一般的な商業ゲームで用いられている,キャラクターAIのアルゴリズムについて調べて,取り入れることにしました.

キャラクタAIのアルゴリズムの比較

 キャラクタAIについては,以下の資料の「Chapter3:意思決定アルゴリズム」が詳しいです.

三宅陽一郎. "ディジタルゲームにおける人工知能技術の応用の現在 (< 特集> エンターテイメントにおける AI)." 人工知能 30.1 (2015): 45-64.

www.jstage.jst.go.jp

 この特集によると,キャラクタAIを作る代表的な意思決定アルゴリズムとして次のようなものが知られています.

Rule-base

 条件と命令をペアにしたルールを登録しておき,これを基に意思決定をする手法です.

State-base

 複数の状態を定義しておき,その間を遷移させていくことで行動を変化させていく手法です.

Behavior-base (Behavior Tree)

 キャラクタの取るべき振る舞いをツリー構造で持っておく手法です.ゲーム業界やロボット分野で幅広く使われている標準的な手法です.作成するユーティリティが揃っていればプログラマ以外も設計をすることができます.

Goal-base

 長期的な目標を見据えたタスク設計をする手法です.ゴールを決めて,そこから逆算していくことで,取るべき行動を決定します.

Utility-base 

 評価関数を持っておき,その値を最大化するような行動を選択することで,キャラクタの行動を作る手法です.

Task-base

 ゴールに対応するタスクを分解して階層化することで,キャラクタの実行すべき仕事を決定する手法です.

Simulation-base

 簡易化した世界の中でランダム要素を組み合わせながらシミュレーションして,最良の結果になるものを選択することで,キャラクタの次の行動を決定する手法です.

 デバッグの容易性という部分も重要ですが,我々のゲームにおいてはもう1人の開発者(非プログラマ)もキャラクタの行動を作成するため,開発者間で簡単に振る舞いについて情報を共有できることが重要です.こうした背景からBehavior Treeを今回は選択することにします.

Behavior Treeの参考実装

 Behavior Treeについては,さまざまな実装例が存在すると思いますが,今回は,以下を参考実装とします.

www.behaviortree.dev

 このサイトでは,Behavior Treeの詳細な説明とともに,C++での実装が紹介されています.

Behavior Treeについて

 Behavior Treeは,あるエージェント(ゲームのNPCなど)の行動を,あらかじめ準備しておいたツリーをもとに決定する手法です.エージェントは一定時間ごとにツリーをたどり,次にやるべき行動をトリガーします.ゲームのNPCなどによく使われる手法ですが,ロボティクスなどの文脈でも広く用いられています.

 上図に,Behavior Treeの構造を示しました.Behaviro Treeを構成するノードを大まかに分類すると,末端ノード,制御ノード,修飾ノードの3種類があります.

末端ノード

 末端ノードはキャラクタの具体的な動作に対応しているノードです.キャラクタの具体的な動作として,「走る」「攻撃する」「回復する」などの行動のほか,「敵が近いかどうかを判定する」「回復をすべきかどうかを判定する」という状況判断があります.

制御ノード

 制御ノードは,処理の流れを制御するためのノードです.自身の配下にあるノードを,一定の方法で順番に実行していきます.

 制御ノードの種類によって処理方法が異なりますが,代表的なものとしてはSequenceNodeがあり,上図に示しているように配下のノードを左端から順番に全て実行していきます.

修飾ノード

 修飾ノードは他のノードの処理を変化させるノードです.配下のノードに対して有効で,処理結果を反転させたり,規定の回数だけ同じ処理を繰り返したりと言ったことができます.

 上図では,末端ノードに対して修飾ノードを適用する例を示していますが,制御ノードに対しても適用できます.

各ノードの処理結果について

 Behavior Treeが大まかには3種類のノードによって構成させることがわかりました.ここで,もう一つ重要な話題があり,それがノードの処理結果です.

 BehaviroTreeでは各ノードは処理結果を自身の上位ノードに通知することになっています.処理結果としては,SUCCESS,FAILURE,RUNNNINGの3種類があります.

 SUCCESSは処理が終了し,かつ,結果が成功であったことを示します.

 FAILUREは処理が終了したけれども,結果が失敗であったことを示します.

 RUNNINGは処理を実行している途中であることを示します.

 ゲーム上の処理の具体例として,例えば,目的地への移動タスクを考えます.このタスクにおいての処理結果を上図に示しました.キャラクタが無事に目的地へ到着した時,このタスクは終了となり,かつ結果が成功であるので,SUCCESSが返されます.反対になんらかのエラーによって目的地への移動が困難になった場合もこのタスクは終了しますが,目的は果たせていないので,FAILUREが返されます.タスクを遂行している途中では,この処理に対応するノードはRUNNNINGを返し続けます.

Tick()について

 ここまでで,Behavior Treeがおおまかに3種類のノードで構成され,それぞれのノードがSUCCESS, FAILURE, RUNNINGのいずれかの処理結果を返すということがわかりました.最後に,これらを使って,どのようにキャラクタの振る舞いが作られるのかを説明します.

 Behavior Treeでは,一定のタイミングごとにTreeを辿ることによって,処理が進んでいきます.Treeを一回辿る処理をTick()と呼んでいて,Tick()では最上位のrootノードから,再帰的にBehavior Treeを辿っていきます.

 Tick()の動作を上図に示しました.Treeを辿って行くと最終的には末端ノードに辿り着き,これによって,それに紐づくアクションが実行されます.前述の通り,全てのノードは処理結果を上位ノードに通知しますので,末端ノードの処理結果が上位ノードに順番に伝播されていって,最終的にrootノードの処理結果が定まることになります.

 TreeのrootがSUCCESSを返したら,一連の行動が全て終了したということになりますので,また最初からTreeを辿り直して,次の行動を決定します.ゲーム上では,これをひたすらに繰り返していくことになります.

 なお,ここで,末端ノードの特にアクションに関連するノードは,ゲーム世界や現実世界の具体的動作に紐づいています.このため,末端ノードは,関連する動作にかかる時間の分だけRUNNINGを返し続けなければなりません.それを実現するためには,末端ノードに待ち時間を値として持たせておき,ツリーでこのノードに辿り着くたびにそれを減じておけば良いでしょう.そして,行動が終わった時,あるいは持ち時間が0になった時にSUCCESSかFAILUREを返すようにします.

 Tick()を呼び出すタイミングに関しては,様々な実装があると思います.例えば,毎秒ごとにTick()を呼び出すという実装や,毎フレームごとにTick()を呼び出すという実装が考えられます.ゲームの場合はフレームを基準に時間を考えることが多いので,毎フレームとまではいかなくても,固定のフレームごとにTick()を呼び出すという実装が一般的だろうと考えます.あるいは,ターン制で進むゲームシステムの場合であれば,ターンごとに辿るという実装も考えられます.

まとめ

 敵NPCの意思決定アルゴリズムとしてBehavior Treeを採用しました.

 Behavior Treeでは主に3種類のノードを組み合わせてツリーを構築し,それを一定時間ごとに辿ることによってキャラクタを動かします.

次回やること

 次のパートではPart1で紹介したBehaviorTreeのノードをさらに詳細に説明し,Processingを用いて各ノードを実装していく予定です.

他パートへのリンク

Part 1 : 現在地点.この記事です.

Part 2: 

horik.hatenablog.com

Part 3: 工事中

Part 4: 工事中

Python3.13でgdb-pedaが動かない問題を修正する

概要

 この記事では,Python3.13の環境下で,gdb-pedaというgdb拡張機能が正常に動作しない問題を解決します.この問題はすでにgdb-pedaのgithubリポジトリ上で言及されており,修正方法についても示されていますので,それに従って,修正作業を実施します.修正後に,簡単な実行ファイルをgdbに入力し,gdb-pedaが適切に動作していること確認します.

問題の説明

 新プログラミング環境への移行に伴って,今まで使ってきたPythonよりも新しいversionのPythonを導入することにしました.これによって,Pythonのバージョンが3.11から3.13に更新されました.

 しかしながら,これによってgdb-pedaというgdb拡張機能が動作しなくなるという問題が発生しました.僕はすっかりgdb-pedaの表示形式に慣れきってしまっていて,今更デフォルトのgdbのレイアウトで作業をすることは困難です.生産性が低下してしまいます.

 具体的には以下のようなエラーが表示されています.

    Type "apropos word" to search for commands related to "word".
/home/horik/clone/peda/peda.py:151: SyntaxWarning: invalid escape sequence '\['
  p = re.compile("(.*)\[(.*)\]") # DWORD PTR [esi+eax*1]
/home/horik/clone/peda/peda.py:373: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(".*exec file:\s*`(.*)'")
/home/horik/clone/peda/peda.py:550: SyntaxWarning: invalid escape sequence '\d'
  p = re.compile("^(\d*)\s*(.*breakpoint)\s*(keep|del)\s*(y|n)\s*(0x[^ ]*)\s*(.*)")
/home/horik/clone/peda/peda.py:554: SyntaxWarning: invalid escape sequence '\d'
  p = re.compile("^(\d*)\s*(.*point)\s*(keep|del)\s*(y|n)\s*(.*)")
/home/horik/clone/peda/peda.py:567: SyntaxWarning: invalid escape sequence '\d'
  m = re.match("in.*at(.*:\d*)", what)
/home/horik/clone/peda/peda.py:596: SyntaxWarning: invalid escape sequence '\d'
  m = re.match("^(\d*).*", line)
/home/horik/clone/peda/peda.py:916: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile("\s*(0x[^ ]*).*?:\s*([^ ]*)\s*(.*)")
/home/horik/clone/peda/peda.py:918: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile("(.*?)\s*<.*?>\s*([^ ]*)\s*(.*)")
/home/horik/clone/peda/peda.py:937: SyntaxWarning: invalid escape sequence '\['
  p = re.compile(".*mov.*\[esp(.*)\],")
/home/horik/clone/peda/peda.py:969: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(":\s*([^ ]*)\s*(.*),")
/home/horik/clone/peda/peda.py:1116: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(".*?:\s*(.*)")
/home/horik/clone/peda/peda.py:1223: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(".*?:\s*[^ ]*\s*(.* PTR ).*(0x[^ ]*)")
/home/horik/clone/peda/peda.py:1226: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(".*?:\s.*\s(0x[^ ]*|\w+)")
/home/horik/clone/peda/peda.py:1235: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(".*?:\s*[^ ]*\s*(.* PTR ).*\[(.*)\]")
/home/horik/clone/peda/peda.py:1430: SyntaxWarning: invalid escape sequence '\s'
  pattern = re.compile("([^\n]*)\s*  ([0-9a-f][^-\s]*)-([^\s]*) \[.*\]\s([^/]*).*  (.*)")
/home/horik/clone/peda/peda.py:2096: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(".*?0x[^ ]*?\s(.*)")
/home/horik/clone/peda/peda.py:2114: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(".*?0x[^ ]*?\s(.*)")
/home/horik/clone/peda/peda.py:2214: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile("Entry point: ([^\s]*)")
/home/horik/clone/peda/peda.py:2242: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile("\s*(0x[^-]*)->(0x[^ ]*) at (0x[^:]*):\s*([^ ]*)\s*(.*)")
/home/horik/clone/peda/peda.py:2316: SyntaxWarning: invalid escape sequence '\s'
  m = re.findall(".*(0x[^ ]*)\s*%s" % re.escape(symname), out)
/home/horik/clone/peda/peda.py:2416: SyntaxWarning: invalid escape sequence '\['
  p = re.compile(".*\[.*\] (\.[^ ]*) [^0-9]* ([^ ]*) [^ ]* ([^ ]*)(.*)")
/home/horik/clone/peda/peda.py:2474: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile("[^\n]*\s*(0x[^ ]*) - (0x[^ ]*) is (\.[^ ]*) in (.*)")
/home/horik/clone/peda/peda.py:2681: SyntaxWarning: invalid escape sequence '\ '
  if re.search(re.escape(asmcode).replace("\ ",".*").replace("\?",".*"), asmcode_rs)\
/home/horik/clone/peda/peda.py:2681: SyntaxWarning: invalid escape sequence '\?'
  if re.search(re.escape(asmcode).replace("\ ",".*").replace("\?",".*"), asmcode_rs)\
/home/horik/clone/peda/peda.py:2832: SyntaxWarning: invalid escape sequence '\ '
  pattern = re.compile(b'|'.join(JMPCALL).replace(b' ', b'\ '))
/home/horik/clone/peda/peda.py:3414: SyntaxWarning: invalid escape sequence '\['
  m = re.search(".*\[(.*)\]|.*?s:(0x[^ ]*)", exp)
/home/horik/clone/peda/peda.py:3519: SyntaxWarning: invalid escape sequence '\['
  sock = re.search("socket:\[(.*)\]", rpath)
/home/horik/clone/peda/peda.py:3529: SyntaxWarning: invalid escape sequence '\s'
  ppid = re.search("PPid:\s*([^\s]*)", status).group(1)
/home/horik/clone/peda/peda.py:3531: SyntaxWarning: invalid escape sequence '\s'
  uid = re.search("Uid:\s*([^\n]*)", status).group(1)
/home/horik/clone/peda/peda.py:3533: SyntaxWarning: invalid escape sequence '\s'
  gid = re.search("Gid:\s*([^\n]*)", status).group(1)
/home/horik/clone/peda/peda.py:4125: SyntaxWarning: invalid escape sequence '\s'
  p = re.compile(".*?:\s*[^ ]*\s*([^,]*),(.*)")
Python Exception <class 'ModuleNotFoundError'>: No module named 'six.moves'
/home/horik/.gdbinit:1: Error in sourced command file:
Error occurred in Python: No module named 'six.moves'
(gdb) 

 reの書き方とsixというモジュールに関しての問題があるようですね.

github上での言及と作業内容の整理

github.com

 この問題について調べてみたところ,githubのpedaのリポジトリで以上のようなissueを発見しました.このissueでは解決方法についても言及されているので,その通りに修正してみたいと思います.

 作業手順は以下の通りです.

  1. コード上で正規表現のr-prefixを補う
  2. six moduleをインストールする
  3. pedaのリポジトリに含まれているsix.pyを削除する

作業

コード上で正規表現のr-prefixを補う

pedaのディレクトリの直下に配置されているpeda.pyを編集します.出力されているWarningに従って,正規表現のパターンが記載されている文字列の直前に r というprefixを以下のように補っていきます.

151行目

    p = re.compile(r"(.*)\[(.*)\]") # DWORD PTR [esi+eax*1]

373行目

    p = re.compile(r".*exec file:\s*`(.*)'")

550行目

    p = re.compile(r"^(\d*)\s*(.*breakpoint)\s*(keep|del)\s*(y|n)\s*(0x[^ ]*)\s*(.*)")

554行目

    p = re.compile(r"^(\d*)\s*(.*point)\s*(keep|del)\s*(y|n)\s*(.*)") 

567行目

    m = re.match(r"in.*at(.*:\d*)", what)

596行目

    m = re.match(r"^(\d*).*", line)

916行目

    p = re.compile(r"\s*(0x[^ ]*).*?:\s*([^ ]*)\s*(.*)")

918行目

    p = re.compile(r"(.*?)\s*<.*?>\s*([^ ]*)\s*(.*)")

937行目

    p = re.compile(r".*mov.*\[esp(.*)\],") 

969行目

     p = re.compile(r":\s*([^ ]*)\s*(.*),") 

1116行目

     p = re.compile(r".*?:\s*(.*)") 

1223行目

     p = re.compile(r".*?:\s*[^ ]*\s*(.* PTR ).*(0x[^ ]*)") 

1226行目

    p = re.compile(r".*?:\s.*\s(0x[^ ]*|\w+)")

1235行目

    p = re.compile(r".*?:\s*[^ ]*\s*(.* PTR ).*\[(.*)\]")

1430行目

    pattern = re.compile(r"([^\n]*)\s*  ([0-9a-f][^-\s]*)-([^\s]*) \[.*\]\s([^/]*).*  (.*)")

2096行目

    p = re.compile(r".*?0x[^ ]*?\s(.*)")

2114行目

    p = re.compile(r".*?0x[^ ]*?\s(.*)")

2214行目

    p = re.compile(r"Entry point: ([^\s]*)")

2242行目

    p = re.compile(r"\s*(0x[^-]*)->(0x[^ ]*) at (0x[^:]*):\s*([^ ]*)\s*(.*)")

2316行目

    m = re.findall(r".*(0x[^ ]*)\s*%s" % re.escape(symname), out)

2416行目

    p = re.compile(r".*\[.*\] (\.[^ ]*) [^0-9]* ([^ ]*) [^ ]* ([^ ]*)(.*)")

2474行目

    p = re.compile(r"[^\n]*\s*(0x[^ ]*) - (0x[^ ]*) is (\.[^ ]*) in (.*)")

2681行目

    if re.search(re.escape(asmcode).replace(r"\ ",r".*").replace(r"\?",r".*"), asmcode_rs)\

2832行目

    pattern = re.compile(r'|'.join(JMPCALL).replace(r' ', r'\ '))

3414行目

    m = re.search(r".*\[(.*)\]|.*?s:(0x[^ ]*)", exp)

3519行目

    sock = re.search(r"socket:\[(.*)\]", rpath)

3529行目

    ppid = re.search(r"PPid:\s*([^\s]*)", status).group(1)

3531行目

    uid = re.search(r"Uid:\s*([^\n]*)", status).group(1)

3533行目

    gid = re.search(r"Gid:\s*([^\n]*)", status).group(1)

4125行目

    p = re.compile(r".*?:\s*[^ ]*\s*([^,]*),(.*)")

six moduleをインストールする

 続いては,sixというPythonのモジュールを取得します.sixについて調べてみると,2系のPythonと3系のPythonの差異を吸収するためのライブラリのようです.

pedaのディレクトリの配下で仮想環境を作ってpipでsixをインストールします.

 (master) horik@horikArch[~/clone/peda]$ ls
LICENSE  README  README.md  lib  peda.py  python23-compatibility.md
 (master) horik@horikArch[~/clone/peda]$ python -m venv peda_env
 (master) horik@horikArch[~/clone/peda]$ source ./peda_env/bin/activate
 (master) horik@horikArch[~/clone/peda]$ pip install six
Collecting six
  Using cached six-1.17.0-py2.py3-none-any.whl.metadata (1.7 kB)
Using cached six-1.17.0-py2.py3-none-any.whl (11 kB)
Installing collected packages: six
Successfully installed six-1.17.0
 (master) horik@horikArch[~/clone/peda]$     

pedaのリポジトリに含まれているsix.pyを削除する

最後に,pedaの配下にある/libに含まれているsix.pyは不要ですので,削除しておきます.

 (master) horik@horikArch[~/clone/peda]$ ls 
LICENSE  README  README.md  lib  peda.py  peda_env  python23-compatibility.md
 (master) horik@horikArch[~/clone/peda]$ ls ./lib
__pycache__  config.py  nasm.py  shellcode.py  six.py  skeleton.py  utils.py
 (master) horik@horikArch[~/clone/peda]$ rm ./lib/six.py 
 (master) horik@horikArch[~/clone/peda]$     

動作確認

 ここまでの作業でgdb-pedaを修正することができましたので,適当なプログラムを投げ込んで動作を確認してみます.

 テスト用に以下のような簡単なプログラムを作成しました.

#include

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

このプログラムをgccコンパイルして,gdbに入力します.

 すると,画像のようにgdb-pedaが正常に動作して,コードの情報が色々と表示されていることが確認できました.

以上で,作業は完了となります.

gdb-pedaの今後について

 gdb-pedaのリポジトリを確認すると,どうも5年以上放置されている様子です.今回扱った問題を解決しようとするPull Requestもいくつか存在しているが,一向にmergeされない様子です.Open Source Projectは結局はボランティア的な部分が強いので,開発者を責めることはできませんが,メンテナンスされなくなったソフトウェアに未来がないことは事実でしょう.それはgdb-pedaも同様ですので,別ツールへの乗り換えが可能であれば,このタイミングで検討すべきだと思います.

まとめ

 Python3.13の環境では,gdb-pedaが正常に動作しませんので,この問題を修正しました.

 pedaはもはやメンテナンスされておらず,可能であれば,別ツールへの移行を検討すべきです.

 

Linuxのしくみを読んだ

概要

 試して理解 Linuxのしくみという本を読んだので,感想を書きます.

本の内容

 Linuxを主な題材に,システムコールやプロセス,メモリ管理,ファイルシステム,仮想化など,オペレーティングシステム使われる諸概念がわかりやすく解説しています.

 実装にはあまり深く立ち入らず,それらの概要の理解することが重視されています.しかし,概論にとどまらない,Linuxの動作をベースにした理解を得ることも目的になっていて,それを達成するために,実験を実施しながら各機能を見ていく構成になっています.

 主題はLinuxではあるのですが,扱われる概念自体はOS分野で広く使われているものですので,OSの本として読むこともできそうです.

 OSの自体の概論とは別に,sarによるシステム情報の観測,ファイルシステムごとの実装の違い,cgroupなど,具体的な手法や実装に言及する場面もあり,学ぶところが多くありました.

読んだ感想

コンセプトがはっきりしている

 実装に深く立ち入らず,図表と実験でわからせる,というコンセプトが貫徹されていて非常に良かったです.本書の目的はOSにあまり詳しくない人がLinuxカーネルという広大な世界について少しでもわかるようになる,ということのようです.これは非常に難しい要求のようですが,図表と実験に重きを置く本書の構成は見事にそれを達成しているように思います.

 以上のコンセプトにより,この本はLinuxの解説書というよりはガイドブックという位置付けになっていて,この本を出発点として,Linuxのさまざまな分野に手を伸ばしていくということができるようになっていると思います.

 こうした本は業界内にも少ないので,Linuxの実装を学び始めようという人にとっては非常に価値のあるものになると思います.

コードはC言語の方が良かった

 この本の実験のコードの記述にGo言語を利用していますが,一つ前の版ではC言語を利用していたようです.大学の講義で教科書としてこの本が指定される場合もあると思いますが,CS学科の学生のほとんどはGoにあまり馴染みがないと思われますので,言語は引き続きCを続投する形が良かったかと思います.

更なるステップについて

LFSに挑戦したい

 LFS (Linux From Scratch)という学習があり,これはソースコードからLinuxをビルドしていって,最終的に自分なりのカスタムLinuxを作るという内容のようです.ずっと興味があったのですが,多分能力不足だろうと思って取りかかれずにいました.ただ,今回OSの講義を取るとともにこの本を読んだことで,で多少なり自信をつけたので,挑戦してみようと思いました.

文献情報

武内 覚.[試して理解] Linuxのしくみ ー実験と図解で学ぶOS、仮想マシン、コンテナの基礎知識【増補改訂版】.初版.技術評論者.2022年10月