KazuminEngine

プログラマーの日記

golangコンパイラ(cgo)のスケジューリングパスについて

こんにちは かずみんです。

今回は、golangコンパイラ(cgo)の最適化パスの一つ、スケジューリングを軽く説明します。

go/schedule.go at dev.boringcrypto.go1.8 · golang/go · GitHub

筆者の言っていることは間違っているかもしれませんので、悪しからず。

スケジューリングの最適化パスは、cpuがパイプラインしやすいように、コンパイラ側で実行順序を変更してあげる最適化です。

パイプラインのデータハザードや構造ハザードに対処する感じだと思います。

どういった順序に直してあげればいいのかは、以下のconstに書かれています。 phi関数はブロックの一番上で実行しないといけないから、一番上、という感じです。 どこに分岐したらいいのかを最後に決めたいので、ScoreControlは最後になっています。

以下のコードのはじめのrangeで、このOpのValueのidは、この順位と振り分けていっています。

その次のブロックでSelectXで、tupleになんやかんややっていますが、これはmipsだけなので、あまり気にしなくてよくて、その下のdefaultだけを着目すればよく、シンプルにconstの順に並べていっている感じだと思います。

スケジューリングパスは、こんな感じで終わり。

ハザードの対応は、この後に機械語の命令に変換するときに、データハザードなどに対応しているんじゃないかな?と思います。

そこでスーパースカラやVLIWのための処理をしているのではないか?と予測します。

また、今後読んでいきたいです。

const (
    ScorePhi = iota // towards top of block
    ScoreArg
    ScoreNilCheck
    ScoreReadTuple
    ScoreVarDef
    ScoreMemory
    ScoreReadFlags
    ScoreDefault
    ScoreFlags
    ScoreControl // towards bottom of block
)

func schedule(f *Func) {
.
.
.
    for _, b := range f.Blocks {
        // Compute score. Larger numbers are scheduled closer to the end of the block.
        for _, v := range b.Values {
            switch {
            case v.Op.isLoweredGetClosurePtr():
                // We also score GetLoweredClosurePtr as early as possible to ensure that the
                // context register is not stomped. GetLoweredClosurePtr should only appear
                // in the entry block where there are no phi functions, so there is no
                // conflict or ambiguity here.
                if b != f.Entry {
                    f.Fatalf("LoweredGetClosurePtr appeared outside of entry block, b=%s", b.String())
                }
                score[v.ID] = ScorePhi
            case v.Op == OpAMD64LoweredNilCheck || v.Op == OpPPC64LoweredNilCheck ||
                v.Op == OpARMLoweredNilCheck || v.Op == OpARM64LoweredNilCheck ||
                v.Op == Op386LoweredNilCheck || v.Op == OpMIPS64LoweredNilCheck ||
                v.Op == OpS390XLoweredNilCheck || v.Op == OpMIPSLoweredNilCheck ||
                v.Op == OpRISCV64LoweredNilCheck || v.Op == OpWasmLoweredNilCheck:
                // Nil checks must come before loads from the same address.
                score[v.ID] = ScoreNilCheck
            case v.Op == OpPhi:
                // We want all the phis first.
                score[v.ID] = ScorePhi
            case v.Op == OpVarDef:
                // We want all the vardefs next.
                score[v.ID] = ScoreVarDef
            case v.Op == OpArgIntReg || v.Op == OpArgFloatReg:
                // In-register args must be scheduled as early as possible to ensure that the
                // context register is not stomped. They should only appear in the entry block.
                if b != f.Entry {
                    f.Fatalf("%s appeared outside of entry block, b=%s", v.Op, b.String())
                }
                score[v.ID] = ScorePhi
            case v.Op == OpArg:
                // We want all the args as early as possible, for better debugging.
                score[v.ID] = ScoreArg
            case v.Type.IsMemory():
                // Schedule stores as early as possible. This tends to
                // reduce register pressure. It also helps make sure
                // VARDEF ops are scheduled before the corresponding LEA.
                score[v.ID] = ScoreMemory
            case v.Op == OpSelect0 || v.Op == OpSelect1 || v.Op == OpSelectN:
                // Schedule the pseudo-op of reading part of a tuple
                // immediately after the tuple-generating op, since
                // this value is already live. This also removes its
                // false dependency on the other part of the tuple.
                // Also ensures tuple is never spilled.
                score[v.ID] = ScoreReadTuple
            case v.Type.IsFlags() || v.Type.IsTuple() && v.Type.FieldType(1).IsFlags():
                // Schedule flag register generation as late as possible.
                // This makes sure that we only have one live flags
                // value at a time.
                score[v.ID] = ScoreFlags
            default:
                score[v.ID] = ScoreDefault
                // If we're reading flags, schedule earlier to keep flag lifetime short.
                for _, a := range v.Args {
                    if a.Type.IsFlags() {
                        score[v.ID] = ScoreReadFlags
                    }
                }
            }
        }
    }
.
.
.
    for priq.Len() > 0 {
            // Find highest priority schedulable value.
            // Note that schedule is assembled backwards.

            v := heap.Pop(priq).(*Value)

            // Add it to the schedule.
            // Do not emit tuple-reading ops until we're ready to emit the tuple-generating op.
            //TODO: maybe remove ReadTuple score above, if it does not help on performance
            switch {
            case v.Op == OpSelect0:
                if tuples[v.Args[0].ID] == nil {
                    tuples[v.Args[0].ID] = make([]*Value, 2)
                }
                tuples[v.Args[0].ID][0] = v
            case v.Op == OpSelect1:
                if tuples[v.Args[0].ID] == nil {
                    tuples[v.Args[0].ID] = make([]*Value, 2)
                }
                tuples[v.Args[0].ID][1] = v
            case v.Op == OpSelectN:
                if tuples[v.Args[0].ID] == nil {
                    tuples[v.Args[0].ID] = make([]*Value, v.Args[0].Type.NumFields())
                }
                tuples[v.Args[0].ID][v.AuxInt] = v
            case v.Type.IsResults() && tuples[v.ID] != nil:
                tup := tuples[v.ID]
                for i := len(tup) - 1; i >= 0; i-- {
                    if tup[i] != nil {
                        order = append(order, tup[i])
                    }
                }
                delete(tuples, v.ID)
                order = append(order, v)
            case v.Type.IsTuple() && tuples[v.ID] != nil:
                if tuples[v.ID][1] != nil {
                    order = append(order, tuples[v.ID][1])
                }
                if tuples[v.ID][0] != nil {
                    order = append(order, tuples[v.ID][0])
                }
                delete(tuples, v.ID)
                fallthrough
            default:
                order = append(order, v)
            }

            // Update use counts of arguments.
            for _, w := range v.Args {
                if w.Block != b {
                    continue
                }
                uses[w.ID]--
                if uses[w.ID] == 0 {
                    // All uses scheduled, w is now schedulable.
                    heap.Push(priq, w)
                }
            }
            for _, w := range additionalArgs[v.ID] {
                uses[w.ID]--
                if uses[w.ID] == 0 {
                    // All uses scheduled, w is now schedulable.
                    heap.Push(priq, w)
                }
            }
        }

HAYABUSAのなかみ(Hayabusa internals)

こんにちは、かずみん (@k47_um1n) | Twitterです。

2021/12/25に、OSS Hayabusa がリリースされたのは、知っていますか? 知らない人向けに簡単に説明すると、”Windowsイベントログのファストフォレンジックタイムライン生成およびスレットハンティングツール”です。 詳しく知りたい人は、日本語REDMEを見てみるとよいです。

また、コアコミッターの いちび (@itiB_S144) | Twitter さんの記事を先に見てみるとよいです。

Windowsイベントログ解析ツール「Hayabusa」を使ってみる - itiblog

この記事では、コミッターである私がHayabusaの中身について、コードベースで説明していきます。 先に、READMEに沿って、ツールを使ってみると、解説が理解しやすいと思います。

v1.0.0の解説です。 Hayabusaは、Rustで書かれています。

※私の解説した処理以外の機能もあるので、ご注意。基本的に、私の興味があるところを解説します。

ディレクトリ構成

ルール処理に関することは、 hayabusa/src/detections/rule ディレクトリに書かれており、

この中のエントリーポイントは、hayabusa/src/detections/detection.rshayabusa/src/detections/mod.rsなどから呼び出されています。

検知処理

検知処理の私の面白そうな部分としては、ルールで論理条件を指定できるcondition項目があります。 そこでは、count処理とand/or/notを指定できます。 以下では、その2つの処理を見ていきたいと思います。

処理の概要

インタプリタ言語処理みたいに、このようになってます。

tokenize -> parser -> eval

parser部分では、SelectionNode木を作成しています。

eval部分では、この木をselect()で実行しています。

この後も、最後に説明するcount処理が入ったりします。

入口

とりあえず、main関数を見ていく。検知部分までは、駆け足でいきます。 githubで見ながら説明を見るとわかるかも。

hayabusa/main.rs at v1.0.0 · Yamato-Security/hayabusa · GitHub

関数フローは、以下のようになります。

- main()
    - exec()
        - analysis_files()
            - parse_rule_files()
                -  rule.init()
                    - compile_condition()
            - analysis_file()
                -  detection.start()
            - add_aggcondition_msges()

exec()の中で、各オプションごとの処理を分岐していきます。 ログファイルが指定されたら、それをanalysis_files()に渡す。 ログディレクトリが指定されたら、それをanalysis_files()に渡す。 両方とも配列にして渡す。

parse_rule_files()は、その名の通り、全部のルールをパースする。パースした時に、オプションに従って、ファイルを除外したりしている。condition項目をパースする処理も含まれ、compile_condition()でパースする。あとでこの処理も説明する。

analysis_file()は複数ファイルを単独で処理するために、このログを検知してくれと、analysis_files()から呼ばれる。alalysis_file()は、and/or/not処理用です。add_aggcondition_msges()がcount処理用です。つまり、これらにフォーカスして見ていけばよい。

ここからは、次の章で個別に見ていく。

条件式パース

結構、わかりやすい日本語のコメントを読むと、理解できる。

pub enum ConditionToken {
    LeftParenthesis,
    RightParenthesis,
    Space,
    Not,
    And,
    Or,
    SelectionReference(String),

    // パースの時に上手く処理するために作った疑似的なトークン
    ParenthesisContainer(Vec<ConditionToken>), // 括弧を表すトークン
    AndContainer(Vec<ConditionToken>),         // ANDでつながった条件をまとめるためのトークン
    OrContainer(Vec<ConditionToken>),          // ORでつながった条件をまとめるためのトークン
    NotContainer(Vec<ConditionToken>), // 「NOT」と「NOTで否定される式」をまとめるためのトークン この配列には要素が一つしか入らないが、他のContainerと同じように扱えるようにするためにVecにしている。あんまり良くない。
    OperandContainer(Vec<ConditionToken>), // ANDやORやNOT等の演算子に対して、被演算子を表す
}

トークンがenumで定義されています。ConditonTokenの中にConditonTokenの配列があり、入れ子になってます。 Rustにはパターンマッチがあるので、便利。

 fn to_enum(&self, token: String) -> ConditionToken {
        if token == "(" {
            return ConditionToken::LeftParenthesis;
        } else if token == ")" {
            return ConditionToken::RightParenthesis;
        } else if token == " " {
            return ConditionToken::Space;
        } else if token == "not" {
            return ConditionToken::Not;
        } else if token == "and" {
            return ConditionToken::And;
        } else if token == "or" {
            return ConditionToken::Or;
        } else {
            return ConditionToken::SelectionReference(token.clone());
        }
    }

文字列からenumトークンに変更する関数

pub static ref CONDITION_REGEXMAP: Vec<Regex> = vec![
        Regex::new(r"^\(").unwrap(),
        Regex::new(r"^\)").unwrap(),
        Regex::new(r"^ ").unwrap(),
        Regex::new(r"^\w+").unwrap(),
    ];

正規表現を配列で定義している。括弧と空白と文字。

Regex::new()はコストのかかる処理で、都度呼び出すと処理速度に大きな影響を与えます。呼び出し回数を削減するために、lazy_staticを使ってRegexを使いまわすようにしています。

 fn compile_condition_body(
        &self,
        condition_str: String,
        name_2_node: &HashMap<String, Arc<Box<dyn SelectionNode>>>,
    ) -> Result<Box<dyn SelectionNode>, String> {
        let tokens = self.tokenize(&condition_str)?;

        let parsed = self.parse(tokens)?;

        return self.to_selectnode(parsed, name_2_node);
    }

トークナイズをやって、パースする。

ConditionTokenをSelectNodeに変更するのがto_selectnode()。これが大事。

fn tokenize(&self, condition_str: &String) -> Result<Vec<ConditionToken>, String> {
        let mut cur_condition_str = condition_str.clone();

        let mut tokens = Vec::new();
        while cur_condition_str.len() != 0 {
            let captured = self::CONDITION_REGEXMAP.iter().find_map(|regex| {
                return regex.captures(cur_condition_str.as_str());
            });
            if captured.is_none() {
                // トークンにマッチしないのはありえないという方針でパースしています。
                return Result::Err("An unusable character was found.".to_string());
            }

            let mached_str = captured.unwrap().get(0).unwrap().as_str();
            let token = self.to_enum(mached_str.to_string());
            if let ConditionToken::Space = token {
                // 空白は特に意味ないので、読み飛ばす。
                cur_condition_str = cur_condition_str.replacen(mached_str, "", 1);
                continue;
            }

            tokens.push(token);
            cur_condition_str = cur_condition_str.replacen(mached_str, "", 1);
        }

        return Result::Ok(tokens);
    }

これは、トークナイザー。トークンに分ける処理ですね。 let token = self.to_enum(mached_str.to_string()); で文字列をトークンに変換して、tokens.push(token); でConditionToken型の配列に入れていますね。

トークンに一致しないと、エラーを出していて、

空白だったら、continueしてますね。

self::CONDITION_REGEXMAP.iter().find_map(|regex| {
                return regex.captures(cur_condition_str.as_str());
            });

これは、正規表現の配列を前から実行していて、先頭で一致した文字列を返している。

fn parse(&self, tokens: Vec<ConditionToken>) -> Result<ConditionToken, String> {
        // 括弧で囲まれた部分を解析します。
        // (括弧で囲まれた部分は後で解析するため、ここでは一時的にConditionToken::ParenthesisContainerに変換しておく)
        // 括弧の中身を解析するのはparse_rest_parenthesis()で行う。
        let tokens = self.parse_parenthesis(tokens)?;

        // AndとOrをパースする。
        let tokens = self.parse_and_or_operator(tokens)?;

        // OperandContainerトークンの中身をパースする。(現状、Notを解析するためだけにある。将来的に修飾するキーワードが増えたらここを変える。)
        let token = self.parse_operand_container(tokens)?;

        // 括弧で囲まれている部分を探して、もしあればその部分を再帰的に構文解析します。
        return self.parse_rest_parenthesis(token);
    }

構文解析をする肝心のパーサーです。parse_rest_parenthesis()で最後に再帰的に構文解析しているらしい。 「ConditionToken::ParenthesisContainerに変換しておく」のがみそだと思う。

fn parse_and_or_operator(&self, tokens: Vec<ConditionToken>) -> Result<ConditionToken, String> {
 for (i, token) in tokens.into_iter().enumerate() {
.
.
.
            if (i % 2 == 1) != self.is_logical(&token) {
                // インデックスが奇数の時はLogicalOperatorで、インデックスが偶数のときはOperandContainerになる
                return Result::Err(
                    "The use of a logical operator(and, or) was wrong.".to_string(),
                );
            }

            if i % 2 == 0 {
                // ここで再帰的にAND,ORをパースする関数を呼び出す
                operand_list.push(token);
            } else {
                operator_list.push(token);
            }
        }

        let mut operant_ite = operand_list.into_iter();
        let mut operands = vec![operant_ite.next().unwrap()];
        for token in operator_list.iter() {
            if let ConditionToken::Or = token {
                // Orの場合はそのままリストに追加
                operands.push(operant_ite.next().unwrap());
            } else {
                // Andの場合はANDでつなげる
                let and_operands = vec![operands.pop().unwrap(), operant_ite.next().unwrap()];
                let and_container = ConditionToken::AndContainer(and_operands);
                operands.push(and_container);
            }
        }
.
.
.
}

and orをパースする。長いので割愛してるが重要なところだけを説明する。 i % 2 == 0 のところが重要。どれがオペランドかオペレータかを判別している。iは配列のインデックスであり、括弧があっても、その数式で判別できる。

たとえば、

A and C and B という入力があると、

AndContainer([AndContainer([ SelectionReference("A"), SelectionReference("C")]), SelectionReference("B")])

を生成する。

これは、木になっており、あとで解説するiter().all()によって、上から再帰的に実行される。

if let ConditionToken::AndContainer(sub_tokens) = token {
            let mut select_and_node = AndSelectionNode::new();
            for sub_token in sub_tokens.into_iter() {
                let sub_node = self.to_selectnode(sub_token, name_2_node)?;
                select_and_node.child_nodes.push(sub_node);
            }
            return Result::Ok(Box::new(select_and_node));
        }

これは、to_selectnode()の一部。ConditionTokenからSelectionNodeへ変更してる。 こんな感じに、tokenの中のsub_tokensを再帰している。

こんな感じに、SelectionNode木が作成される。

countパース

rule/ディレクトリに入ってるコードは、日本語コメントがあって優しい。

pub static ref AGGREGATION_REGEXMAP: Vec<Regex> = vec![
        Regex::new(r"^count\( *\w* *\)").unwrap(), // countの式
        Regex::new(r"^ ").unwrap(),
        Regex::new(r"^by").unwrap(),
        Regex::new(r"^==").unwrap(),
        Regex::new(r"^<=").unwrap(),
        Regex::new(r"^>=").unwrap(),
        Regex::new(r"^<").unwrap(),
        Regex::new(r"^>").unwrap(),
        Regex::new(r"^\w+").unwrap(),
    ];

さっきと同様に、正規表現を配列で用意している。

pub enum AggregationConditionToken {
    COUNT(String),   // count
    SPACE,           // 空白
    BY,              // by
    EQ,              // ..と等しい
    LE,              // ..以下
    LT,              // ..未満
    GE,              // ..以上
    GT,              // .よりおおきい
    KEYWORD(String), // BYのフィールド名
}

トークンを定義。入れ子には、なっていない。

 pub fn compile(&self, condition_str: String) -> Result<Option<AggregationParseInfo>, String> {
        let result = self.compile_body(condition_str);
.
.
.
}

コンパイルエントリ。compile_body()でメイン処理をする。

pub fn compile_body(
        &self,
        condition_str: String,
    ) -> Result<Option<AggregationParseInfo>, String> {
        // パイプの部分だけを取り出す
        let captured = self::RE_PIPE.captures(&condition_str);
        if captured.is_none() {
            // パイプが無いので終了
            return Result::Ok(Option::None);
        }
        // ハイプ自体は削除してからパースする。
        let aggregation_str = captured
            .unwrap()
            .get(0)
            .unwrap()
            .as_str()
            .to_string()
            .replacen("|", "", 1);

        let tokens = self.tokenize(aggregation_str)?;

        return self.parse(tokens);
    }

パイプが無いとエラーが起きる。ソースコードから、conditionの書き方が分かったりもする。 パイプを削除してから、トークナイザーを実行。その処理は、and/or/notとほぼ同じなので、割愛。 パーサーを見てみよう。

と思ったけど、ソースコードのコメント見てね。

https://github.com/Yamato-Security/hayabusa/blob/main/src/detections/rule/aggregation_parser.rs#L134

and/or/not検知(eval)

関数フローのdetection.start()で並列に解析を実行していく。

Rustの優秀な関数を使うので、便利。

// Ruleファイルの detection- selection配下のノードはこのtraitを実装する。
pub trait SelectionNode: mopa::Any {
    // 引数で指定されるイベントログのレコードが、条件に一致するかどうかを判定する
    // このトレイトを実装する構造体毎に適切な判定処理を書く必要がある。
    fn select(&self, event_record: &EvtxRecordInfo) -> bool;

    // 初期化処理を行う
    // 戻り値としてエラーを返却できるようになっているので、Ruleファイルが間違っていて、SelectionNodeを構成出来ない時はここでエラーを出す
    // AndSelectionNode等ではinit()関数とは別にnew()関数を実装しているが、new()関数はただインスタンスを作るだけにして、あまり長い処理を書かないようにしている。
    // これはRuleファイルのパースのエラー処理をinit()関数にまとめるためにこうしている。
    fn init(&mut self) -> Result<(), Vec<String>>;

    // 子ノードを取得する(グラフ理論のchildと同じ意味)
    fn get_childs(&self) -> Vec<&Box<dyn SelectionNode>>;

    // 子孫ノードを取得する(グラフ理論のdescendantと同じ意味)
    fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode>>;
}
mopafy!(SelectionNode);

このトレイトと満たすものは、中のメソッドは呼べる。

 pub fn select(&mut self, event_record: &EvtxRecordInfo) -> bool {
        let result = self.detection.select(event_record);
        if result && self.has_agg_condition() {
            count::count(self, &event_record.record);
        }
        return result;
    }

これは、impl RuleNodeのselect()。

self.detectionは、SelectionNode型なので、これを満たす構造体は、select()を呼べる。

AndSelectionNode, NotSelectionNode, OrSelectionNode, RefSelectionNode, LeafSelectionNodeがSelectionNodeトレイトを満たす。

それぞれ、同じようなselect()になっていて、andのselect()実装を見てみる。

 fn select(&self, event_record: &EvtxRecordInfo) -> bool {
        return self.child_nodes.iter().all(|child_node| {
            return child_node.select(event_record);
        });
    }

iter().all()が非常に優秀。入れ子になっているSelectionNodeがすべてtrueだったら、trueを返す。

orだったら、iter().any()を呼ぶ。どれかひとつでもtrueだったら、true。 この仕組みで、論理式を判定している。

再帰入れ子を実行実行していくと、最後には、 LeafSelectionNode構造体を実行することになり、これのselect()が以下のようになっている。

            let values = utils::get_event_value(&"Event.EventData.Data".to_string(), &event_record.record);
            let eventdata_data = values.unwrap();
            if eventdata_data.is_boolean() || eventdata_data.is_i64() || eventdata_data.is_string()
            {
                return self
                    .matcher
                    .as_ref()
                    .unwrap()
                    .is_match(event_record.get_value(self.get_key()), event_record);
            }

レコード(evtxの中の1つ分)からEvent.EventData.Data項目を取ってきて、boolかi64かstringだったら、is_matchを読んで確認する。(これは一部で、他の処理もしている)

ここからはmatcherに入るので、少々複雑。私があまり興味ないので、雑です。 self.matcherの中身は、RuleNode.detectionが初期化されるときに、作られる。

LeafSelectionNode構造体のselect()の最初の

if self.matcher.is_none() {
            return false;
}

は、conditionの中の変数の中身が定義されていないなら、ルールにマッチしないことを返す。

self.matcherにandとorで使う変数の名前と中身が保存されている。

is_match()で変数の中身とレコードの中身が正規表現で一致すれば、trueを返す。

count処理

timeframeについて書く。

pub fn aggregation_condition_select(rule: &RuleNode) -> Vec<AggResult> {
    // recordでaliasが登録されている前提とする
    let value_map = &rule.countdata;
    let mut ret = Vec::new();
    for (key, value) in value_map {
        ret.append(&mut judge_timeframe(&rule, &value, &key.to_string()));
    }
    return ret;
}

これがcount処理のエントリーポイント。カウントしたい文字列ごとにjudge_timeframe()を呼んでますね。 並列化をしてないようです。今後そうすると、早くなるかもしれませんね。

judge_timeframe()を見ていきましょう。

pub fn judge_timeframe(
    rule: &RuleNode,
    time_datas: &Vec<AggRecordTimeInfo>,
    key: &String,
) -> Vec<AggResult> {
    let mut ret: Vec<AggResult> = Vec::new();
    if time_datas.is_empty() {
        return ret;
    }

    // AggRecordTimeInfoを時間順がソートされている前提で処理を進める
    let mut datas = time_datas.clone();
    datas.sort_by(|a, b| a.record_time.cmp(&b.record_time));

    // timeframeの設定がルールにない時は最初と最後の要素の時間差をtimeframeに設定する。
    let def_frame = &datas.last().unwrap().record_time.timestamp()
        - &datas.first().unwrap().record_time.timestamp();
    let frame = get_sec_timeframe(rule).unwrap_or(def_frame);

    // left <= i < rightの範囲にあるdata[i]がtimeframe内にあるデータであると考える
    let mut left: i64 = 0;
    let mut right: i64 = 0;
    let mut counter = _create_counter(rule);
    let data_len = datas.len() as i64;
    // rightは開区間なので+1
    while left < data_len && right < data_len + 1 {
        // timeframeの範囲にある限りrightをincrement
        while right < data_len && _is_in_timeframe(left, right, frame, &datas) {
            counter.add_data(right, &datas, rule);
            right = right + 1;
        }

        let cnt = counter.count();
        if select_aggcon(cnt as i64, rule) {
            // 条件を満たすtimeframeが見つかった
            ret.push(counter.create_agg_result(left, &datas, cnt, key, rule));
            left = right;
        } else {
            // 条件を満たさなかったので、rightとleftを+1ずらす
            counter.add_data(right, &datas, rule);
            right += 1;
            counter.remove_data(left, &datas, rule);
            left += 1;
        }
    }

    return ret;
}

ルールで「24時間内にログインが5回以上あったら攻撃と見なす」などと書ける。timeframeは、この24時間のこと。

区間問題系のアルゴリズムを使っている。 尺取り法というアルゴリズムを参考にしています。それにより、計算量を愚直に計算したO(N*N)より早くしています。(参考資料)

recordの中にtargetの文字列が存在するかどうかの判定は、RuleNode構造体を初期化するときにselect()で処理しています。それを終えているとして、話を進めます。

まず、時間順にソートする。 leftが区間の左端を指していて、rightが区間の右端を指している。 leftもrightも、0で初期化されている。0でframe内であれば、countとrightをインクリメントする。 条件を満たさなくなると、ルールを満たしているか判断するselect_aggcon()を実行する。by、<、>、などを満たしていると、必要な情報を ret.push して、leftにrightを代入して、またそこから開始する。満たしてなければ、leftとrightをインクリメントして、timeframe区間ごと、右に移行してする。

区間がtimeframe内かどうかを判定するのは、以下の _is_in_timeframe()で処理をしている。

fn _is_in_timeframe(left: i64, right: i64, frame: i64, datas: &Vec<AggRecordTimeInfo>) -> bool {
    let left_time = _get_timestamp(left, datas);
    let left_time_nano = _get_timestamp_subsec_nano(left, datas);
    // evtxのSystemTimeは小数点7桁秒まで記録されているので、それを考慮する
    let mut right_time = _get_timestamp(right, datas);
    let right_time_nano = _get_timestamp_subsec_nano(right, datas);
    if right_time_nano > left_time_nano {
        right_time += 1;
    }
    return right_time - left_time <= frame;
}

小数点の細かいところまで考慮しているそうです。

right_time - left_time <= frame こんな感じで判定しているね。

ちょっと長いので、select_aggcon()はURLを張り付けておきますね。

https://github.com/Yamato-Security/hayabusa/blob/fddcbc7e7d5c96cb2e35efe5d401549eab997f0a/src/detections/rule/count.rs#L246

以上です。最後まで読んでくださって、ありがとうございます。

最後に

OSS Hayabusaは大和セキュリティーにより、開発されました。開発者やAD hacking(ルール作成)などを募集しています。 興味ある方は、以下のURLでslackに参加して、#hayabusa-*チャンネルで自己紹介から始めてください。ちょっと、話だけでも聞きたい!でも大丈夫です。お待ちしております。

また、

Hayabusaのバグや追加してしい機能などの報告は、GitHub issueで受け付けております。なお、githubがよくわからない方は、slackで気軽に意見を募集しております。

Slack

この記事をきっかけにHayabusaに興味を持ってくれると、幸いです。

それでは!よい、セキュリティーライフを!!

golang(cgo)コンパイラのcopyelim最適化パスについて

golangコンパイラの処理系のcgoコンパイラの最適化パスを適当に解説していく記事です。

今回は、copy eliminationです。copy 駆除(削除)ですね。

たぶん、以下を持って助長だと判断して、elimするようです。

a = b;c = a;

だと思います。

c = b 

でいいよね。たぶん。

COPY

opのCOPYってのは、型変換みたいなmov命令系のことだと思います。

機械語を吐く、以下のコードから推測しました。

ちなみに、レジスタが同じだったら、そもそも機械語を吐かないことになってますね。

case ssa.OpCopy: // TODO: use MOVLreg for reg->reg copies instead of OpCopy?
        if v.Type.IsMemory() {
            return
        }
        x := v.Args[0].Reg()
        y := v.Reg()
        if x != y {
            opregreg(s, x86.AMOVL, y, x)
        }

cgo

肝心の処理系のコードですが、説明むずかしいので、ほぼ無しです

めっちゃ適宜コード削除しているので、しっかり読みたい方は、本家を見ましょう。

go/copyelim.go at master · golang/go · GitHub

func copyelim(f *Func) {
    //すべてのvalueの引数(arg)の0番目のopがCOPYだったら、wに入れて、そして、wをvの引数に戻す。
    //a = b;c = a; が c = bになる感じだと思います多分。
    //コピーが入れ子になってるから、浅くしようね。といった感じです。

    for _, b := range f.Blocks {
        for _, v := range b.Values {
            w := v.Args[0]

            //適宜書き換えたので、無限ループになっちゃう感じだけど、
            //forじゃなくてifの方がわかりやすいかもしれない
            for w.Op == OpCopy {
                w = w.Args[0]
            }

            for v != w {
                x := v.Args[0]
                v.SetArg(0, w)
                v = x
            }
        }
    }
}

golang(cgo)コンパイラの共通部分式除去について

こんにちは、かずみんです。

golangの最適化パスを読んでいて、なんとなく、理解できたものを軽く紹介していくスタイル。連載するかどうかは、分からない。間違ってる部分とかあると、思います。

cgoってのは、golangコンパイラの一種です。何も考えずにインストールすると、大体これ使ってます。

共通部分式除去( Common subexpression elimination, CSE )は、コンパイラの最適化の一つで、同じ計算式が現れるところを 一つの変数に置き換えられるかを計算して、できるなら置き換えるもの。簡単に説明すると、同じ式を何回もやっちゃうと、時間がかかるので、計算しなくても、一度計算したものに置き換えようね。って感じ。

コードは、シンプルに理解するために、適宜削除しているコードなどがあります。完全版が見たければ、githubに行きましょう。

go/cse.go at master · golang/go · GitHub

zero CSEというパスもありますが、それは別になっています。

子の最適化パスが大まかにやっている処理としては、こんな感じ。

三つめが肝心なのだが、コードを見てくれ

以下のコードには、適宜コメントで説明している。

func cse(f *Func) {
    Valueポインタ型の配列をf.NumValues()個用意する。
    a := make([]*Value, 0, f.NumValues())

    //f.auxmapがnilだったら、f.auxmapを初期化してあげる。
    if f.auxmap == nil {
        f.auxmap = auxmap{}
    }
    
    //TypeがMemoryではないすべてのValueを最初に用意したa変数に追加していく。
    //その際に、一つ条件がある。
    //auxmap[v.Aux]が0だったら、f.auxmap配列の長さに1を足したものをそれに入れる。
    for _, b := range f.Blocks {
        for _, v := range b.Values {
            if v.Type.IsMemory() {
                continue // memory values can never cse
            }
            if f.auxmap[v.Aux] == 0 {
                f.auxmap[v.Aux] = int32(len(f.auxmap)) + 1
            }
            a = append(a, v)
        }
    }

    //パーティションを計算する
    partition := partitionValues(a, f.auxmap)

    //パーティションからクラスに変換する。
    //すべてのvalueをv.IDが添え字なvalueEqClassにidをマイナスにして、ストアする。
    valueEqClass := make([]ID, f.NumValues())
    for _, b := range f.Blocks {
        for _, v := range b.Values {
            // Use negative equivalence class #s for unique values.
            valueEqClass[v.ID] = -v.ID
        }
    }
    var pNum ID = 1

    //パーティションのすべてのvalueにpNumを与える。
    //大事なのは、みんなバラバラってところ。あとで、やるけど、肝心なのは、同じなら、という処理。
    for _, e := range partition {
        for _, v := range e {
            valueEqClass[v.ID] = pNum
        }
        pNum++
    }

    // 
    // 等価クラスを、等価でない引数があるポイントで分割します。 分割が見つからないまで繰り返します。
    var splitPoints []int
    byArgClass := new(partitionByArgClass) // reuseable partitionByArgClass to reduce allocations
    for {
        changed := false
 
        //パーティっしょん配列を先頭から走査。
        for i := 0; i < len(partition); i++ {
            e := partition[i]

            if opcodeTable[e[0].Op].commutative {
                //比較の前に、二つの引数(Args)を並び替えます。
                for _, v := range e {
                    if valueEqClass[v.Args[0].ID] > valueEqClass[v.Args[1].ID] {
                        v.Args[0], v.Args[1] = v.Args[1], v.Args[0]
                    }
                }
            }

            // 頑張って、ソート。引数の eq クラスでソート
            byArgClass.a = e
            byArgClass.eqClass = valueEqClass
            sort.Sort(byArgClass)

            // 分割ポイントを見つける
            splitPoints = append(splitPoints[:0], 0)
            for j := 1; j < len(e); j++ {
                v, w := e[j-1], e[j]
                // Note: commutative args already correctly ordered by byArgClass.
                eqArgs := true
                for k, a := range v.Args {
                    b := w.Args[k]
                    if valueEqClass[a.ID] != valueEqClass[b.ID] {
                        eqArgs = false
                        break
                    }
                }
                if !eqArgs {
                    splitPoints = append(splitPoints, j)
                }
            }
            if len(splitPoints) == 1 {
                continue // no splits, leave equivalence class alone.
            }

            // e の代わりに別の等価クラスを下に移動します。
            partition[i] = partition[len(partition)-1]
            partition = partition[:len(partition)-1]
            i--

            // 見つけた e の部分に対して新しい等価クラスを追加します。
            splitPoints = append(splitPoints, len(e))
            for j := 0; j < len(splitPoints)-1; j++ {
                f := e[splitPoints[j]:splitPoints[j+1]]
                if len(f) == 1 {
                    // Don't add singletons.
                    valueEqClass[f[0].ID] = -f[0].ID
                    continue
                }
                for _, v := range f {
                    valueEqClass[v.ID] = pNum
                }
                pNum++
                partition = append(partition, f)
            }
            changed = true
        }

        if !changed {
            break
        }
    }

  厳密支配を手に入れる(strictly dominate)
    sdom := f.Sdom()

    // v と w が同じクラスにあり、v が w を支配する場合は、v を w に置き換えます。
    rewrite := make([]*Value, f.NumValues())
    byDom := new(partitionByDom) // reusable partitionByDom to reduce allocs
    for _, e := range partition {
        byDom.a = e
        byDom.sdom = sdom
        sort.Sort(byDom)
        for i := 0; i < len(e)-1; i++ {
            // eはドミネーターによりソートされているので、一番大きいドミネータ要素は、スライスの先頭にある。
            v := e[i]
            e[i] = nil

            // メインの入れ替え作業
            for j := i + 1; j < len(e); j++ {
                w := e[j]
                //vがwのアンセスターだったら、入れ替えをrewriteに記録。そうじゃなきゃforを抜け出す。
                if sdom.IsAncestorEq(v.Block, w.Block) {
                    rewrite[w.ID] = v
                    e[j] = nil
                } else {
                    // e is sorted by domorder, so v.Block doesn't dominate any subsequent blocks in e
                    break
                }
            }
        }
    }

    rewrites := int64(0)

    // rewriteに記録していたものを実際に適用する。
    for _, b := range f.Blocks {
        for _, v := range b.Values {
            for i, w := range v.Args {
                //変更したいのは、w.IDを添え字にしたrewrite配列に入ってるから、それを取り出して、
                vのi番目にストア。
                if x := rewrite[w.ID]; x != nil {
                    v.SetArg(i, x)
                    rewrites++
                }
            }
        }
        //コントロールバリューってのも変えとかなくちゃいけない。
        for i, v := range b.ControlValues() {
            if x := rewrite[v.ID]; x != nil {
                b.ReplaceControl(i, x)
            }
        }
    }

}

今後

今後は、パスの一つ一つがどんなのか、軽く説明していきたい気持ちがある。専門的に知らなくても、日常的な言葉でわかるものが結構ある。

全然説明になってないじゃないか。と思う人もいるかもしれませんが、最適化に興味を持つ人がでてくると、幸いです。

VulsがAlma Linux/Rocky Linuxをサポートした。

vulsがAlma LinuxとRocky Linuxをサポートしました。

feat(os) : support Rocky linux by kazuminn · Pull Request #1260 · future-architect/vuls · GitHub feat(os) : support Alma Linux by kazuminn · Pull Request #1261 · future-architect/vuls · GitHub

これにより、今年でサポートが終わるCentOS8からそれらに乗り換えても、vulsによるスキャンを継続することができます。

この際、パッチ当たるのが早いUbuntuをお使いになるのもありだと思います。ちなみに、UbuntuもVulsでスキャンすることができます。

詳しくは、こちらを → https://vuls.io/

なお、CentOS7は2024/6/30までサポートしているようです。

ご使用に関してですが、

データベースとして、redhatのovalをそのままご使用いただけるので、

$ goval-dictionary fetch-redhat 5 6 7 8

とすればよく、以前と変わらずお使いいただけると思います。

いつかの心...SecHack365....!

こんにちは、かずみんです。

SecHack365の3期生です。2019年。

終了してから一年が経つので、今更感ありますが、経験したことなどを書いていきます。あまり長い文量は、書きません。

「文章が短い」ということは、「学んだことが少ない」ということではないので、注意しましょう。

開発経験

私は、大学の講義で、一週間ほどグループで開発したりしましたが、SecHack365でエミュレーターを開発するまでは、長期間の開発経験というものはありませんでした。

また、コードを毎日書いていき、胸を張って「作った」と言えるものはありませんでした。

そうゆう意味で、長期間にほぼ毎日コードを書いて作品を作る経験はとても良い経験になりました。

自立支援

私は、軽度の難聴なのです。SecHack365では、多種多様な人の意見を聞いたりするのに、耳で聞いたりしないといけなかったですが、SecHackが始まった当初は、自分の耳との付き合い方がわかっていませんでした。

しかし、運営の方のサポートもあり、聞こえない自分と人との接し方の相談に乗ってもらったり、応援してもらったりして、自立するための基礎力がついたと思います。

「耳が悪いのでもっと大きな声で言ってください。」と言うなどの技を学びました。最初は、それが言えないほどだった....

また、発表など時は、マイクを通して、字幕を作成してもらうなどの支援をしてもらいました。

トレーナーなどのあなたが耳が悪いことを知っているから大丈夫、などかなり良心的な雰囲気も作ってくれました。

後、アンケートなどで講演をゆっくりはっきり喋ってくれ、とリクエストすると次にそうなっていた記憶があります。アンケートは、素直に書きましょう。運営が良心的にめっちゃ頑張ってくれるので、ちゃんと書こう。(こうゆうところ、すごく教育に情熱を傾けてるな、と感じる)

SecHack365の前に、Hardeing(イベント)があった時は、うまくコミュニケーションが取れませんでしたが、終了後に同じようなイベントがオンラインでありましたが、結構コミュニケーション取れた気がするので、

SecHack365で成長できた気がします。

とてもいい経験になったので、今後どこでもやっていける自信がついた気がします。

これから、応募する人は、もし直したいところや成長したいところがあれば、受かってから相談するといいです。例えば、人付き合いが下手で、もっとうまくひとと付き合いたいとか。(もっとうまく付き合いたい。と思っているのがポイント。思ってることはすごいこと、それを伝えよう。)。後、自閉症傾向があって不安とか、鬱病が治ったばかりで心配とか。絶対協力してくれます。勇気を出して、言いましょう。また、車椅子生活していて大丈夫ですか?などだと参加申し込む前に相談しましょう。2020年は、オンラインなので、ちょっとわからないですが。

修了後

SecHack365では、typetalkを使って、連絡やらコミュニケーションを行うのですが、私は、自分専用のトピック(部屋のようなもの)を作って、トレーナー、一期二期生を全部入れています。自分専用トピックを作っている人はちらほらいるのですが、全期横断個人チャンネルにしたのはたぶん私だけです。

たまに、いろんな人からいいねがつくので、結構見てくれているのだなー。と思っています。

話が脱線しそうになりましたが、

私はこのトピックにTwitterのように呟いています。

たまに、エミュレーター作りで、全部お世話になった川合さんから素敵な助言が入ったりしました。(これが嬉しくて、私はたびたび幸せな気持ちになりました。)

ある日SecHack365で作ったエミュレータに細工するとハイパーバイザーになるんじゃないかと降ってきまして、(私は前々からそれが作りたかった。)それを呟くと、なんと坂井先生が「できるよと」、そして、ロジックやら細かいことを教えてくれました。

そのアイディアでサイボウズラボユースに応募するとなんと受かりました。

そのトピックに毎日のようの呟いてきたおかげで、ラボユースに受かることができたと思っています。

ラボユースの契約は2021 1月 - 3月で終わりましたが、以下の記事がその私の成果発表の補足ブログです。

sigactionシステムコールによるホスト型ハイパーバイザー開発調査~サイボウズ・ラボユース10期生~ - KazuminEngine

あとで、ラボユースのブログの方で、またまとめが記事が投稿されると思います。

まとめ

2021年4月からは、また、新たなことが始まりますが、今後どうなっていくのか楽しみで仕方ありません。 おわり。

sigactionシステムコールによるホスト型ハイパーバイザー開発調査~サイボウズ・ラボユース10期生~

こんにちは、かずみんです。この記事は、サイボーズラボユースの発表会の持ち時間が7分しかなく、詳細なことが話せないので、発表を補うことが目的です。と言っても、掲示板のメモを時系列順に書いて行っただけの物になったりしています。

まずは、どのようにして応募に至ったか

SecHack365のIMでいろいろ呟いているのだけれど、次のように投稿したら、坂井さんから「できるよ」とのことで、「signalシステムコール を使えば、できちゃうと思います。」とのことで、作り始めたのが始まり。私は、前々からハイパーバイザー に憧れがあり作りたくもあり、手始めにエミュレータから作り始めたのである。

投稿:「閃いたんだけど、このエミュレーターに追加実装で、センシティブじゃ無い命令を直接実行させてあげて、ゲスト用のプロセスを作ってやるとハイパーパイザーになりそうだけど。なります?」

システムコール を使うような実装は、初めてだったので一ヶ月ぐらいかけて、signalシステムコール などを使って、cpuで命令を直接実行し、センシティブな命令をトラップするプロトライプが完成した。

基本的な実装方法は、坂井さんのホームページの

各種実験用サンプル

に書いてある。基本的なロジックはこのページに書いてあるので、読んでください。

一月ぐらいかかったのは、osをメモリに読み込んで、実行し、エミュレートした際に、EIPを更新するのを忘れて、同じ命令を繰り返し実行しちゃうとかで、いろいろ試行錯誤していたからである。結果的にはEIP++したけど、試行錯誤している段階では、iretしてコンテキストスイッチして解決しようなどとしていた。また、結果的には使ってないのだけれど、get_sigframe() setup_sigcontext() struct sigframe struct sigcontextをインポートするのにも時間がかかった。(それも初めてだった。)また、__builtin_frame_addressを使って、ゲスト実行時のコンテキスト情報がうまくとれなかったのも原因。これもかなり時間がかかって、諦めて、代わりに、sigactionシステムコール を使うことで、ゲスト実行時のコンテキスト情報を取得した。ちなみに、sigactionの方を使うのがモダンらしい。signalシステムコール をは古くて、代わりにsigactionシステムコール を使ってね。とマニュアルに書いてある。坂井さんがsignalシステムコール を用いて実装しているのは、一般にそれを使っている人が多いので、理解しやすく受け入れやすいためだそうだ。

ここらへんで、12月中頃にラボユースに応募したら、面接して受かったので、1/8日頃から契約を開始した。c++の書き方などがいろいろわからなかったので、『C/C++によるソフトウェア開発』の光成メンターに応募した。ちなみに、面接の時に、星野さんがいたけど、それ以来一度も関わっていない。

面接では、「VMMを作りたい」と話したところ、よくわかってもらえなかったので、なぜか考えていたら、自分の作りたいものはハイパーバイザー だったので、そのことをあとでメールして光成さんと星野さんに伝えてもらった。また、そこまで実装した分で「

ホスト型ハイパーバイザー の作り方 part.1 - KazuminEngine

」という記事を書いたので、メールを送って、二人にリンクを共有してもらった。私のhvの基本ロジックは、この記事に書いてあるので、これを読むと、あとの説明がより理解できると思います。

このメールは気軽に送れた。相手側が原因で、面接が五分程度遅れたこともあり、「何か質問などがございましたら、メールしてください」と言われていたからだ。もちろん、この記事をみて、応募しようかなと思った人も、面接開始時間が遅れなくても、気軽に質問メールなどを送って欲しい。

ラボユース開始

活動中は、サイボウズOfficeの個人作業掲示板にTwittterみたいに、やったこと悩んでいることメモなど、綴っていくのだけれど、それを時系列順に書いていく。決して、見やすいことはないだろうが書いていく。一行が一つの仕事の単位だと思って、読んでいただくと。

作業を開始する前に、メンターの光成さんと打ち合わせをした。勤務の仕方など3月までのゴールについて話した。ここでは、三月までにはりぼてosの画面描画を成功させることを目標としようと話した。なぜ、はりぼてosを動かすのかというと、私がSecHack365で作ったはりぼてOSが動くエミュレータを拡張して実装していくからである。極論、はりぼてOSをうごかしたい思いはないが、xv6などを動かす前に機能が少ないはりぼてosをまずは動かせるようにしようとの魂胆である。また、始まる前は毎日フルで入るつもりだったが、様子見で、二週間程度、月水金で入ることが決まった。

まず、リポジトリを作った。https://github.com/kazuminn/ghyper

メモリ上にあるバイナリをステップ実行する術を探していたら、坂井先生のハローワールド本にちょうど書いていたのを発見した。layout asmすればよかった。 また、display/i $pcとすれば、ステップ実行ごとに命令を表示できることをメンターさんに教えてもらった。前者だと、表示が崩れたり、終了すると画面に何も残らないので、後者を使うことにした。

次に、紹介するのはかなり大きな問題だった。push %es命令がシグナルを出して来れなくて、例外捕虜できない問題にぶち当たった。gdbデバッグして、SIGILLシグナルが連続して発行されていることが確認された。なので、例外ハンドラー実行中に例外が発生しているのが原因なんじゃないかと思い。ハンドラー実行中に例外を発行できないようにした。設定したけど、現状変わらないので、どうやら、それが原因じゃないことがわかった。悩んでいると、毎週水曜日の定例が開催され、そこで、詰まっていることを話したら、どうしていくのがいいのかを教えてもらった。qemu上で同じことをしたら、push %esしたら、大丈夫かどうかをまずは確認してね。とのことである。さらに、正しく動いているものを確認しましょう。とのことで、最小構成のpush %esが動く小さいプログラムが動くコードを書いて、それを実行して、HVのコードで動かないことがあるなら、コメントアウトなどをして、動く状態に持っていき、それからコードを戻しながら、動かなくなった時を見つけて、その原因を探るのが良いと教えてもらいました。バイナリエディタでpush %csに変えると上手く動くことが判明し、push %esの一つ上から順に一つ上に開始位置を変更して実行して、動かなくなる命令を発見した。それは、espを変更する命令だった。それが原因で、スタックを変更するといろいろ狂うのがわかった。その命令をバイナリエディターでnop(何もしない命令)に変更すると、push %esが例外ハンドリングされた。どうやって、解決したらいいのか案が浮かばないので、坂井さんに状況を説明すると、sigaltactionシステムコール とSA_ONSTACKを使って、シグナル処理ようのスタックを別に割り当てると回避できるかもしれない。とのことであった。三十分ぐらいで実装を完了して、この件は解決した。

ここぐらいから、10:00-17:00勤務して行った。夕方だれるので、五時まで。

開発はiMacのメモリ4GBでしているのだが、ブラウザなどの他のアプリを落とさないと、実行できなくなってきたので、物理メモリを12GBに増やした。4000円ほどで購入した。

次に、lock wbinvd命令にぶち当たったのだが、これがおかしかった。objdumpしたものと、ソースコードが違うので、なんでだろうと、悩んでいると。メンターさんによると、wbinvd命令にはlockプレフィックスはつかないらしい。いろいろ考えて判明したのだが、osのはじめは16bit codeなので、16bitモードで実行しないといけなかった。 面倒だったので、32bitモードから開始することにした。push %esもプレフィックスがずれていて、そんな命令実行されないようだった。

とりあえず、eipを飛ばすだけで、32bitモードに入った。

mov_rm32_imm命令を実装。エミュレーターのmodrm関数を流用して、ちょっと変えて、使った。

mov_rm32_r32命令を実装

ここで、レジスタ退避とレジスタ復帰が必要なことがわかり、例外ハンドラーのはじめと終わりに挟むことにした。実装したんだけど、ずっとバグってる。

エミュレーションをインラインアセンブリでできないかと試行錯誤してみたが、結構複雑になり、結局それはやめた。

エミュレーターのメモリバッファを~4GB領域に配置できれば、ほぼトラップされないので、早くなるんじゃないかと考えたが、そこにメモリバッファを置くのは不可能だったので、諦めた。

ここで、push/popが普通に実行されることを話すと、それだと、メモリ不整合になるという話になりました。エミュレーターのメモリバッファ上で作業しているのに、hvのメモリをpush/popで利用しちゃうと、不整合になる。いろいろ案を出したのだが、それだと、上手くいかないとメンバーさんが言い、理由を説明してもらって、納得した。最終的に解決策として、threadを作り、その中で、allocaシステムコール でスタック全体を確保し、push/popで例外ハンドラーを起こすようにした。やり方は、まず、pthreadが動く小さいプログラムを書いて、そこで動いたのが確認できたら、hvのプロジェクトに打ち込んだ。

eflagsのレジスタ退避ができなくて、悩んでいたが、popfを使えば、できそう。と思った。

c++のgoogle testをプロジェクトに組み込んだ。オプションの順番でエラーがでて、少々てこずった。また、main関数周りでエラーが起きて、かなりてこずった。五時間ぐらいかかったのではないだろうか。c++は慣れていないので、大変だった。rustやgolangだと、組み込みのunit testがあるので、なんて便利なんだ思った。

64ビット環境で32ビットがどうやって実行されるかの質問をしていた。少し、理解した。

mov edx, esp; mov eax, 3; push eax; mov ecx, [edx] ; pop eax; バイト列が実行できるようにした。(レジスタ退避と復帰はバグているが

一つ問題を見つけました。

40 00 04 48というバイト列は 32bitモードでは

(A) inc eax add byte ptr [eax+ecx*2], al

64bitモードでは (B) add byte ptr [rax+rcx*2], al です。

だから64bit環境で32bitバイナリで(A)のつもりでビルドされたバイト列を実行すると(B)になってしまいます。そしてraxやrcxの値によってはそのまま動いてしまうでしょう。

との指摘が。

もうちょっと、詳しく説明すると、正確には、[rax+rcx*2]ですが、説明のためにこれをraxと簡略化します。 64bitモードで実行可能な32bit命令において、32bitのeax(raxの下位32bit)が、64bitとしてcpuに直接実行してもらうと、eaxがraxになる。もし、raxが正常値でsigsegvが起こらない時は、ハンドリングのしようがなく、実行したいeaxに置き換えることはできず、そのまま実行されてしまうので、実装不能である。 rcxも同様。 → 32bithvにしたら、回避できそう。ここを考えていると、契約期限が迫ってきました。

今のところ32ビット開始の以下のところが動いています。(レジスタ退避と復帰は動いていませんが。。。

   319 00000000 55                               PUSH    EBP
   320 00000001 89 E5                               MOV EBP,ESP
   321 00000003 57                                  PUSH    EDI
   322 00000004 56                                  PUSH    ESI
   323 00000005 53                                  PUSH    EBX
   324 00000006 8D 5D A4                            LEA EBX,DWORD [-92+EBP]
   325 00000009 81 EC 00000450                      SUB ESP,1104
   326 0000000F C7 85 FFFFFBE0 FFFFFFFF             MOV DWORD [-1056+EBP],-1
   327 00000019 C7 85 FFFFFBDC 00000000             MOV DWORD [-1060+EBP],0
   328 00000023 C7 85 FFFFFBD8 7FFFFFFF             MOV DWORD [-1064+EBP],2147483647
   329 0000002D C7 85 FFFFFBD4 00000000             MOV DWORD [-1068+EBP],0
   330 00000037 C7 85 FFFFFBC8 00000000             MOV DWORD [-1080+EBP],0
   331 00000041 A0 00000FF1                         MOV AL,BYTE [4081]
   332 00000046 C0 F8 04                            SAR AL,4
   333 00000049 89 C2                               MOV EDX,EAX
   334 0000004B 83 E2 07                            AND EDX,7
   335 0000004E 89 95 FFFFFBC4                      MOV DWORD [-1084+EBP],EDX
   336 00000054 C7 85 FFFFFBC0 FFFFFFFF             MOV DWORD [-1088+EBP],-1
   337 0000005E C7 85 FFFFFBBC FFFFFFFF             MOV DWORD [-1092+EBP],-1
   338 00000068 C7 85 FFFFFBB8 FFFFFFFF             MOV DWORD [-1096+EBP],-1
   339 00000072 C7 85 FFFFFBB4 00000000             MOV DWORD [-1100+EBP],0
   340 0000007C C7 85 FFFFFBB0 00000000             MOV DWORD [-1104+EBP],0