はじめに
前回記事で、シンクライアント端末に極めて向いているという紹介をしたASUS Chromebook CM3206だが、今回はその検証記事となります。
▼レビュー記事はこちら

主に、このChromebookでのLinux(Baguette)の利用について述べていきます。
AIエージェントの操作
AIエージェントを動かすには様々ありますが、ここではGoogleのAntigravityの利用について記載する。
Antigravityは以下の通り種類がある。
1.Antigravity 2.0
スタンドアロンのアプリになっている。チャットのUIでウィンドウが一つ起動する。GoogleのAIプランに応じて使用できるトークン数が決まっている。自然言語で実行タスクの指示が可能で、チャット開始前に以下のようにモデルを選択する。指示内容によって、サブエージェントを自動的に呼び出したり、または単独で処理を行ったりする。ローカルの計算資源が潤沢であったりセキュリティが気になったりする場合はローカル動作するモデルをAntigravity上で実行できる。後はウィンドウのGUIを構えているので、アーティファクトという右隣に別ウィンドウが表示され、成果物を綺麗に見せてくれたりしする。チャットを複数立ち上げてタスクを並行処理することはできるが、別チャットの動作が終わったらその成果物を読み込んで別タスクを実行する、などの指示は元のタスク終わったのを人間が見て手動で引き受けるチャット側で参照の指示をする必要がある。

2.Antigravity CLI
コマンドライン上で動くAntigravity。基本的に2.0と同様の事ができる。得意分野が違う。こちらはLinuxのコマンドで指示をするのでタスクの自動化が得意。一方でGUIの表示をしながら、ユーザとインタラクティブに成果物を確認しながら修正するというようなことは、こちらでは単体では難しい。
こちらもエージェントなのでLinuxコマンドを自分で実行してくれます。

3.Antigravity IDE
統合開発環境。ほぼVS CodeのUIです。自分でコードを書いて、それを確認や修正してもらったりする事ができます。
この他にも1つありますが、ここでは上記3つを記載します。
CM3206 (ARM64 アーキテクチャ)での動作
CM3206はARMアーキテクチャであるMediaTek Kompanio 540で動作している。
このデバイスでAIエージェントを動作させたいので、私はLinux環境を有効にし、上記3つを実行してみることにした。
すると、Antigravity CLIは問題なく動作した。上記のCLIの説明に添付している画像はまさにこのCM3206で動作させたものだ。
しかし、Antigravity 2.0、IDEは実行すると、エラーが起きて立ち上がりが完了しなかった。
まずAntigravity IDEは以下のような感じだ。

・1行目のMmapで、51TBの開始地点から1GBを確保したいと言って、失敗しています。
・2行目でTCMallocが48bitの仮想アドレス空間を前提にしてるからかもと言って、あなたのシステムに
合う(つまり39bit仮想アドレス空間)様にビルドし直してね、と言って来ています。
・3行目で致命的エラーと言ってメモリ不足で、128KBが確保できなかったと言っています。
・その後、SIGABRTでLanguage serverが停止し、プログラムが開始できなかった。
となっています。原因はOSのカーネルが39bitの仮想アドレス空間で構成されているのに対して、プログラムが48bitの仮想アドレス空間を要求したことですね。
次に、Antigravity 2.0は以下のような感じだ。

同じですね。ポップアップでは詳しいログは出てこないですが、同じと思われます。
本エラーに対する対策
1.遠隔サーバーを利用する分離運用スタイルによる突破
1つ目はこれです。シンクライアントデバイスとして非常に優秀なので、同じくリモートデスクトップアプリとして通信量も少なく、鮮明で綺麗な描画を提供してくれるWindows appを使用します。これはWindowsのために作成されたアプリと思いますが、Xrdpとpipewire搭載のLinuxであれば同じくこのアプリで遠隔サーバー(例えば自宅サーバー)にアクセスできます。
ちなみにですが、リモートデスクトップアクセスという観点においてWaylandはおすすめしません。
GNOME環境であればgnome-remote-desktopがあるから可能だと言うかもしれませんが、RDPでアクセスすると、少なくとも自分の環境では謎の薄ピンクの等間隔の◯が背景に見えます。画面が暗くなると分かります。
後、音声の転送がまた自分の環境ではですが、Chromebookの場合、gnome-remote-desktopでは転送されません。Macの場合は、等間隔の◯表示は同じですが、音声は転送されます。ただし転送元側でも鳴ってた気がします。
これらの問題はXrdpとpipewire搭載のLinuxであれば発生しない問題です。
話は逸れましたが、解決策の1つ目がこれで、手元のLinux環境を有効にして、CLIを起動したり(有効にせずSSH接続だけでやっても良いですが)、2.0とIDEはRDPでサーバーに接続し、サーバー上のGUIで操作するという感じになります。
2.39bitの仮想アドレス空間で起動するようにパッチを当て、ローカル起動により突破
2つ目はこれです。ローカルで起動するようにしてしまう方法です。
やり方は、Antigravity CLIに、「Antigravity.tar.gzをダウンロードしてきたから、解凍してantigravityを起動して」と頼みます。
こうすると、Antigravity CLIがエラー原因を特定して、パッチ適用、起動まで持っていってくれます。
具体的には以下が最後のまとめです。 (以下は2.0を起動しようとした場合です。)

これにより、39bit環境でローカル起動が可能になります。
ここでもやはりツールはローカル起動ですが、その中で動かすモデルはクラウドか自宅サーバー内のモデルであったりするわけです。この観点では、サーバーの計算力を活用するという点で1つ目の方法と違いはありません。
ここで、ログが残っているので、CLIがどのようにこのプロセスを完遂したか見てみました。
以下はプロセスが気になる人用です。
ログを見ると、CLIはGitHub上で公開されている wallentx/antigravity-cli-termux という、Termux(Android環境)向けのパッチリポジトリを見つけ出し、そのソースコードを読んで自律的に学習していたことが分かりました。
素晴らしいアプローチを考案し、オープンソースとして公開してくださっている原作者の @wallentx 氏の知見に、この場を借りて感謝いたします。
▼ オリジナルのリポジトリはこちら https://github.com/wallentx/antigravity-cli-termux
実際にCLIが実行したコマンド
CLIは先人の知恵を学習した後、手元の環境に合わせて自律的にPythonスクリプトを生成し、以下のシェルコマンドを実行してバイナリの書き換えと置き換えを全自動で行いました。
# AIが自律的に実行したパッチ適用コマンド
python3 patch_language_server.py /home/username/Antigravity-arm64/resources/bin/language_server
# 1. 元のバイナリをバックアップとして退避
mv /home/username/Antigravity-arm64/resources/bin/language_server /home/username/Antigravity-arm64/resources/bin/language_server.bak
# 2. パッチ適用済みのバイナリを元の名前に置き換え
mv /home/username/Antigravity-arm64/resources/bin/language_server.patched /home/username/Antigravity-arm64/resources/bin/language_server
生成されたPythonスクリプト
CLIがリポジトリの知見を応用し、私の環境に合わせて実際に書き上げたスクリプトがこちらです。 単なる設定ファイルの変更ではなく、完成している実行ファイル(ELFバイナリ)の機械語の16進数を直接読み込み、39bit空間で動くようにピンポイントで書き換えるという、非常に高度なアプローチを行っています。
import hashlib
import shutil
import struct
import sys
from pathlib import Path
src = Path(sys.argv[1])
dst = src.with_name(src.name + ".patched")
print(f"Input binary : {src}")
shutil.copyfile(src, dst)
data = bytearray(dst.read_bytes())
def get(off):
return struct.unpack_from("<I", data, off)[0]
def put(off, word):
struct.pack_into("<I", data, off, word)
lo, hi = 0, len(data)
def find_section(name_target):
if data[:4] != b"\x7fELF":
return None, None
e_shoff = struct.unpack_from("<Q", data, 40)[0]
e_shentsize = struct.unpack_from("<H", data, 58)[0]
e_shnum = struct.unpack_from("<H", data, 60)[0]
e_shstrndx = struct.unpack_from("<H", data, 62)[0]
shstr_base = e_shoff + e_shstrndx * e_shentsize
shstr_off = struct.unpack_from("<Q", data, shstr_base + 24)[0]
for i in range(e_shnum):
base = e_shoff + i * e_shentsize
sh_name = struct.unpack_from("<I", data, base)[0]
sh_offset = struct.unpack_from("<Q", data, base + 24)[0]
sh_size = struct.unpack_from("<Q", data, base + 32)[0]
nend = data.index(b"\x00", shstr_off + sh_name)
section = data[shstr_off + sh_name : nend].decode("utf-8", errors="replace")
if section == name_target:
return sh_offset, sh_offset + sh_size
return None, None
sec_lo, sec_hi = find_section("google_malloc")
if sec_lo is not None:
lo, hi = sec_lo, sec_hi
print(f"Found google_malloc section: file 0x{lo:x} - 0x{hi:x} ({(hi - lo) // 1024} KB)")
else:
print("google_malloc section not found - scanning entire binary.")
ubfx_count = 0
lsl_count = 0
for off in range(lo, hi, 4):
w = get(off)
if (w & 0x7F800000) == 0x53000000: # bitfield-move family
immr = (w >> 16) & 0x3F
imms = (w >> 10) & 0x3F
if immr == 42 and imms == 44: # ubfx Xn, Xm, #42, #3
put(off, (w & ~((0x3F << 16) | (0x3F << 10))) | (35 << 16) | (37 << 10))
ubfx_count += 1
elif immr == 22 and imms == 21: # lsl Xn, Xm, #42 encoded as lsr
put(off, (w & ~((0x3F << 16) | (0x3F << 10))) | (29 << 16) | (28 << 10))
lsl_count += 1
print(f"[1] ubfx patches : {ubfx_count}")
print(f" lsl patches : {lsl_count}")
mask_count = 0
for off in range(lo, hi - 4, 4):
if get(off) == 0x92D3800A and get(off + 4) == 0xF2E0000A:
put(off, 0x9280000A)
put(off + 4, 0xD35DFD4A)
mask_count += 1
print(f"[2] Random mask : {mask_count}")
mmap_count = 0
for off in range(lo, hi, 4):
if get(off) == 0xF2E00029:
put(off, 0xD3596129)
mmap_count += 1
print(f"[3] MmapAligned : {mmap_count}")
word_rewrites = {
0xD2C20009: 0xD2C00409,
0xD2C2000A: 0xD2C0040A,
0xF2C20008: 0xF2DFF408,
0xF2C20009: 0xF2DFF409,
0xD2C10009: 0xD2C00209,
0xD2C1000A: 0xD2C0020A,
0xF2C38008: 0xF2DFF708,
0xF2C38009: 0xF2DFF709,
0x92560A6C: 0x925D0A6C,
0x92560A6A: 0x925D0A6A,
0xD2C3000D: 0xD2C0060D,
0xD2C3000C: 0xD2C0060C,
0xD2C08008: 0xD2C00108,
}
counts = {old: 0 for old in word_rewrites}
for off in range(lo, hi, 4):
w = get(off)
if w in word_rewrites:
put(off, word_rewrites[w])
counts[w] += 1
print(f"[4] Tag constants: {sum(counts.values())} words rewritten")
dst.write_bytes(data)
dst.chmod(0o755)
print(f"Output : {dst}")
⚠️ 注意事項 (Disclaimer) 本記事で紹介しているバイナリパッチの手法は、特定の環境(ASUS CM3206 / 39bit仮想アドレス空間)での検証を目的としたものです。実行ファイルを直接書き換えるため、システムを破壊するリスクがあります。試される場合は必ずバックアップを取得の上、完全な自己責任で実行してください。
まとめ
本記事では、CM3206でのLinux利用、とりわけAIエージェントの利用について検証しました。
CLIはそのまま起動できましたが、IDEと2.0はエラーで起動不可となります。しかし、これらを動かすための選択肢として、2通りの解決策があります。
他のARMアーキテクチャのChromebookでどのようになっているかは不明です。ちなみに手元のPixel 10 Pro Foldについても調べてみましたが、39bitでビルドされていました。
秋にGooglebookが発売予定ですが、intel系は48bitなので問題ないですが、ARM系のチップは大丈夫なんですよね?本記事で検証した通り、39ビットではAntigravityのIDE/2.0が起動できず、ローカル起動にはパッチ適用が必要になります。48bitでビルドしてもらえるか心配ではあります。ハードウェアを作るのはOEMですが、カーネルのビルド設定を決めるのはGoogleですからね。
メモリ節約のために39bitを選ぶ判断も理解できますが、パワーハウス前提のGooglebookであれば、自社が提供するAntigravityをスムーズに動かせる48bitのビルドを期待したいですね。

コメント