LoginSignup
3
0

More than 1 year has passed since last update.

RustでWindowsXP用のコンソールアプリケーションを開発する

Last updated at Posted at 2022-11-13

RustでWindowsXP~Windows11まで共通で動作するexeを作成してみたかったため、
WindowsXPビルド用の環境の構築を行いました。
ソースコードレベルでは.NetFrameworkでも可能ですが、
XPは.NETFramework3.5までしかインストール出来ず、
Windows10等は.NETFramework3.5は別途インストールが必要になります。
以下はWindowsXP用のスタティックリンク用のexeを作成するための手順です。

環境のセットアップ

以下のサイトに従ってセットアップを行いました。

予めRustのstable版が動作する環境が整っていることが前提になります。

WindowsXP用にrustの1.49をインストールします

rustup install 1.49
rustup default 1.49
rustup target install i686-pc-windows-msvc

ビルドする際は以下の環境変数の設定が必要になります。

$env:CARGO_BUILD_RUSTFLAGS = '-C target-feature=+crt-static -C link-arg=/SUBSYSTEM:CONSOLE,5.01'

ビルドします。

cargo build --release --target=i686-pc-windows-msvc

他のプロジェクトと並行して開発を行っている場合環境の切り替えが面倒になります。
以下のスクリプトのように、stableと1.49を切り替えるコマンドを書くと便利です。

build.ps1
rustup default 1.49
$env:CARGO_BUILD_RUSTFLAGS = '-C target-feature=+crt-static -C link-arg=/SUBSYSTEM:CONSOLE,5.01'
cargo build --release --target=i686-pc-windows-msvc
rustup default stable

VSCodeのターミナルで以下のコマンドを実行すれば、元の環境を維持したままXP用のビルドが行なえます。

.\build.ps1

EXEとDLLのバージョンを取得するサンプル

実際にコードを書く際にrustのバージョンと使用できるcrateについて注意が必要になります。
使用したいcrateの

  • 対応するrustのバージョン(1.49以下)
  • WindowsXPのOS機能による制約(async等が使用不可)
  • Cargo.tomlでedition = "2018"にする
    に注意して下さい。
    実際にはビルドしてエラーが出た場合、下位のバージョンを使用してみる等の工夫が必要になります。
Cargo.toml
[package]
name = "rust_xp_sample"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

PEHeaderの解析

バージョンを取得するWindowsAPIがありますが今回は使用しません。
.rsrcセクタにあるVS_FIXEDFILEINFOを取得します。
本来であれば順次リソースのデータを解析してデータを特定する必要がありますが、
VS_FIXEDFILEINFOのSIGNATURE(0xfeef04bd)をリソースから全検索して、
バージョン情報の格納位置を読み取っています。

pe_header.rs
use std::{convert::TryInto, mem::size_of};

#[derive(Debug, Clone, Copy)]
pub struct VersionInfo {
    pub file_version: Version,
    pub product_version: Version,
    pub time_date_stamp : u32,
}

impl VersionInfo {
    pub fn new(
        file_version: Version,
        product_version: Version,
        time_date_stamp : u32,
    ) -> Self {
        Self {
            file_version,
            product_version,
            time_date_stamp
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Version {
    pub majaor: u16,
    pub minor: u16,
    pub build: u16,
    pub private: u16,
}

impl Version {
    pub fn new(majaor: u16, minor: u16, build: u16, private: u16) -> Self {
        Self {
            majaor,
            minor,
            build,
            private,
        }
    }
}

pub fn u32_from_slice(slice: &[u8]) -> u32 {
    u32::from_ne_bytes(slice.split_at(size_of::<u32>()).0.try_into().unwrap())
}

pub fn u16_from_slice(slice: &[u8]) -> u16 {
    u16::from_ne_bytes(slice.split_at(size_of::<u16>()).0.try_into().unwrap())
}

pub fn str_from_slice(slice: &[u8]) -> String {
    std::str::from_utf8(slice)
        .unwrap()
        .trim_matches('\0')
        .to_string()
}

unsafe fn u32_slice_from_slice(p: &[u8]) -> &[u32] {
    std::slice::from_raw_parts(
        p.as_ptr() as *const u32,
        p.len() / std::mem::size_of::<u32>(),
    )
}

pub fn read_version(buf: &[u8]) -> Result<VersionInfo, &str> {
    if buf.len() < 0x400 {
        return Err("ヘッダサイズが足りません");
    }

    // "MZ"
    if u16_from_slice(&buf[0..]) != 0x5a4d {
        return Err("WIN32アプリではありません");
    }

    // "PE"
    let pe_offset = u32_from_slice(&buf[0x3c..]) as usize;
    if u16_from_slice(&buf[pe_offset..]) != 0x4550 {
        return Err("PEフォーマットではありません");
    }

    let time_date_stamp = u32_from_slice(&buf[pe_offset+8..]);

    let mut sector = pe_offset + 0xf8;
    let mut rsrc_data: [u32; 10] = [0; 10];
    let mut read_rsrc = false;

    loop {
        let string = str_from_slice(&buf[sector..sector + 8]);

        if string == "" {
            break;
        }
        if string.trim_matches('\0') == ".rsrc" {
            unsafe {
                let tmp = u32_slice_from_slice(&buf[sector..sector + 40]);
                rsrc_data.copy_from_slice(&tmp);
                read_rsrc = true;
            }
            break;
        }
        sector += 40;

        if sector >= buf.len() {
            break;
        }
    }

    if !read_rsrc {
        return Err(".rsrcセクションが見つかりませんでした");
    }

    // リソースの解析
    sector = rsrc_data[5] as usize;

    // .rsrcを全検索
    const SIGNATURE: u32 = 0xfeef04bd;
    for offset in sector..buf.len() - 4 {
        if u32_from_slice(&buf[offset..]) == SIGNATURE {
            let index = offset + 8;
            return Ok(VersionInfo::new(
                Version::new(
                    u16_from_slice(&buf[index + 2..]),
                    u16_from_slice(&buf[index..]),
                    u16_from_slice(&buf[index + 6..]),
                    u16_from_slice(&buf[index + 4..]),
                ),
                Version::new(
                    u16_from_slice(&buf[index + 10..]),
                    u16_from_slice(&buf[index + 8..]),
                    u16_from_slice(&buf[index + 14..]),
                    u16_from_slice(&buf[index + 12..]),
                ),
                time_date_stamp
            ));
        }
    }

    Err("バージョン情報がありません。")
}

read_version関数で、

  • ファイルバージョン
  • 製品バージョン
  • ビルドタイムスタンプ
    が取得できます。
main.rs
    fn main() -> Result<(), std::io::Error>{
        let buf = fs::read("test.exe")?;
        if let Ok(ver) = read_version(&buf){
            println!(
                "file_version : {}.{}.{}.{}",
                &ver.file_version.majaor,
                &ver.file_version.minor,
                &ver.file_version.build,
                &ver.file_version.private
            );
        }
        Ok(())
    }

以下のように出力されます。

file_version : 1.0.0.0
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0