【初心者向け】GASで始めるプログラミング初歩
本記事は暇つぶし目的で書いたジョーク記事であること、ご了承ください。
※この記事では、Linux x86_64(WSL2やDocker環境含む)を前提に説明します。
めんどくさい仕事たちを目の前に、ちょっとだけブレイクタイム。
週休7日と、ちょっとだけ余裕のある暮らしがあれば満足です。
でも、どちらも満たされないのだから、仕事なんてやってられません。
そんな不満しかない毎日だって、腐ってはいけません。
こうして、ほんの少しだけサボる時間。
そこにだって、僅かながらのうるおいというものがあるのではないでしょうか。
そんなはずはない。
なにがなんでも週休は最低7日必要だろ!とツッコミを入れながら。
少しだけ、私の暇つぶしにお付き合いいただければ幸いです!
「GAS」という言葉を聞いたこと、目にしたことはありますか?
noteでも多くの方が「GAS」を使って業務改善を行う、素晴らしいアイデアや仕組みの公開をされています。
是非 #GAS や #GAS初心者 から、素敵な記事を探してみてください!
ところで……
「スプレッドシートの操作を自動化したい」
「Gmailと連動させてメールを自動送信したい」
そう思ってこの記事を開いてくださったみなさん……
残念でした。
今回扱うのは、GNU Assembler(GAS) です。
そして、安心してください!
Googleのアカウントも、Google Workspaceの契約も一切使いません。
必要なのは
Linux環境(ターミナル)と、CPUと聞いただけで白飯3杯いけるかどうか。
たったそれだけです。
それでは、めくるめくレジスタ操作の世界へ出発です!
そもそも GNU Assembler(GAS)とは?
GAS(GNU Assembler)は、GNUプロジェクトが開発しているアセンブラ(アセンブリ言語を機械語に変換するツール)です。
プログラミング関連の記事でよく目にするPythonやJavaScript、あるいはGoogle Apps Scriptなどは「高レベル言語」と呼ばれ、人間が読みやすいように作られています。
一方、アセンブリ言語は「CPUが直接理解する機械語に対応する、人間が読める形の低レベルな命令表現」です。
0か1だけで構成されているマシン語を、人間にわかりやすい表現を使って書いてある、そんな感じです。
前回の「最初歩」では、スタックを使って高級言語の「関数」呼び出しの基礎的な動きを作りました。
暇人丸出しの第4弾!
「超入門」と「入門」は無事に終えた。
「最初歩」から進むのならば、そこはもう「初歩」です!
いつになったら一歩目を超えるんだ?
そもそも、超える気あるのか?
私にもわかりません!
Google Apps Scriptなら
let arr = [10, 20, 30];
console.log(arr[1]); // 20と書くだけで、複数のデータを並べて好きな位置からポンと取り出せます。
勝手にメモリを確保し、勝手にサイズを伸ばし、安全に値を取り出してくれる。
便利ですね。
では、GNU Assemblerではどうでしょう。
そんな甘やかされた世界はありません。
「配列」なんて便利な構文は存在しません!
あるのは「メモリの連続した番地」と「泥臭い掛け算・足し算」だけです。
環境構築(コマンド一発)
まさかの前回までに試してみてくださったかたは飛ばしてください。
環境構築も一瞬です。
お手元のLinux(WSL2やDocker環境でもOK)のディストリビューションに合わせて、以下のコマンドを実行するだけです。
Ubuntu / Debian / WSL:
sudo apt update && sudo apt install build-essentialFedora / RHEL:
sudo dnf groupinstall "Development Tools"Arch Linux:
sudo pacman -S base-devel「Google Cloud Consoleを開いて、APIを有効化して、OAuth同意画面を設定して、認証キー(JSON)をダウンロードして……」なんて煩わしい設定は一切不要です。
コマンド一発。
最高ですね。
配列の正体:メモリの連続した土地
CPUから見た配列とは何なのか。
低レイヤーの世界で、4バイトの数値が連続してメモリに並んでいるとすると
「同じサイズの箱が隙間なく並んだメモリ上の土地」
というイメージになります。
いやいや、ぴったりサイズの箱を作ってよって思っちゃいますよね。
でも、それはコンピュータの仕組み的に無理。
とても残念です。
たとえば、4バイト(32bit整数)の数値が並ぶ配列を考えてみましょう。
先頭アドレス(Base)
↓
┌──────┬──────┬──────┐
│ 10 │ 20 │ 30 │
└──────┴──────┴──────┘
4バイト 4バイト 4バイト
[インデックス 0] [インデックス 1] [インデックス 2]では、インデックス1の値を取り出すにはどうするのか。
低レイヤーでは、基本的にこう考えます。
欲しいデータの番地 = 先頭アドレス +(インデックス × 要素のサイズ)
今回の例なら、
「先頭番地 +(1 × 4バイト)」
を計算して、その番地にある値(20)を取り出します。
高級言語では arr[1] と書くだけで済む処理も、低レイヤーまで降りてくると、こんな計算が必要になるわけです。
便利なものの裏側には、こういうめんどくさい仕事が待っています。
CPUの秘密兵器:Base-Index-Scale アドレッシング
「毎回そんな掛け算や足し算を手で書くの?」 と思いますよね。
安心してください。
x86_64には、この
「ベースアドレス+インデックス×要素サイズ」
というアドレス計算を、メモリアクセス命令の中で直接指定できる仕組みがあります。
それがこちらです。
(%rdi, %rcx, 4)「なんじゃこの呪文は!?」
とか思っちゃうのは仕方ない。
カッコで括ってるとかLISPかよ!ってつっこんでしまいそうになる。
そして、そんなツッコミをしたところで、カンマ区切りじゃんって言われる。
いや、むしろその前にLISPってなんだよになる。
でも、中身を分解すると極めて合理的です。
%rdi(ベースアドレス): 配列の先頭番地
%rcx(インデックス): 何番目の要素か(0, 1, 2...)
4(スケール): 1要素のサイズ(1、2、4、8バイトのいずれか)
CPUはこの指定をもとに、 %rdi + (%rcx * 4) という実効アドレスを計算し、そのメモリ位置にアクセスしてくれます。
つまり、配列の何番目を読みたいのかを、命令の中にそのまま書けるわけです。
高級言語では
arr[i]の一言で済む処理です。
でも、アセンブラの世界の扉を開けてしまうと
「ベースはここ。インデックスはこれ。1個あたり4バイト」
と、いちいちCPUに説明してあげる必要があります。
高級言語って、親切ですね。
実際に動かしてみよう!
配列 [10, 20, 30] をループで回し、インデックス計算をしながら全要素を足し算して合計(60)を printf で表示してみます。
array_sum.s という名前で保存してください。
.global main
.section .rodata
msg:
.string "Total: %d\n"
# 4バイト整数(.long)が3つ並んだ配列データ
.section .data
my_array:
.long 10
.long 20
.long 30
.text
main:
# --- スタックフレーム作成 ---
push %rbp
mov %rsp, %rbp
sub $16, %rsp # アライメント調整(16バイト確保)
# --- 配列の走査準備 ---
leaq my_array(%rip), %rdi # %rdi に配列の先頭アドレスをセット(ベース)
xor %rcx, %rcx # %rcx = 0(インデックスカウンタ)
xor %eax, %eax # %eax = 0(合計値を入れる箱)
loop_start:
cmp $3, %rcx # インデックスが 3 に達したか?
jge loop_end # 3以上ならループ終了
# 配列要素の取得と加算: %eax += *(%rdi + %rcx * 4)
addl (%rdi, %rcx, 4), %eax
inc %rcx # インデックスを +1
jmp loop_start # ループの先頭へ戻る
loop_end:
# --- printf を呼び出す ---
leaq msg(%rip), %rdi # 第1引数: 書式文字列
movl %eax, %esi # 第2引数: 合計値(60)
xor %eax, %eax # 浮動小数点引数は0個
call printf
# --- スタックフレーム解体 ---
mov $0, %eax # return 0;
leave
retポイント解説
leaq my_array(%rip), %rdi
mov ではなく lea(Load Effective Address)を使っています。
中身の「10」を取り出すのではなく、「配列が置かれている番地そのもの(アドレス)」を %rdi に渡しています。
addl (%rdi, %rcx, 4), %eax:
ここが今回の主役です。%rcx が 0 ➔ 1 ➔ 2 と増えるたびに、CPUが自動で実効アドレスを計算し、配列の中身を取り出して足し込んでいます。
境界チェックなんてない
高級言語では、配列の範囲外をチェックしてくれるものもあります。
一方、アセンブリには、 「これは配列の3個目だからOK、4個目だからエラー」 といった配列そのものの境界概念はありません。
指定されたアドレスを読みに行くだけです。
その先がたまたま読み取り可能なメモリなら、何事もなかったかのように値を読んでしまいます。
そして運悪く、アクセスしてはいけない領域なら、OSからSegmentation Faultをいただくことになります。
「ここから先は立入禁止です」
という看板を、自分で立てておかないといけません。
なんとも自由な世界です。
コンパイルと実行
gcc にアセンブルとリンクをまとめてやってもらいましょう。
Bash
gcc -no-pie array_sum.s -o array_sum
./array_sumPlaintext
Total: 60と表示されれば大成功です!
オブジェクトも、結局はこれと同じ…なのか?
JavaScriptで
let user = {
id: 1,
age: 20
};のようなオブジェクトを作ったとします。
低レイヤーの世界まで降りていくと、データが最終的にメモリ上に存在することには変わりありません。
ただし、JavaScriptのオブジェクトが単純に、 「先頭アドレスから0バイト目が id、4バイト目が age」 という構造になっているわけではありません。
そのあたりはJavaScriptエンジンが面倒を見てくれています。
我々はそんなことを考えず、
user.ageと書けばいい。
高級言語、やっぱり優しい。
このあたりまで自分で面倒を見る必要がないからこそ、我々は本来の仕事に集中できるのです。
…たぶんね。
まとめ:GASをマスターして業務効率化(?)を目指そう!
今回、初歩として
配列の正体(メモリの連続領域)
アドレスそのものを取得する lea 命令
CPUのアドレッシング機構 (%rdi, %rcx, 4)
境界チェックが存在しない低レイヤーの自由(と危険)
を見てきました。
GASをどれだけ極めても、スプレッドシートの自動化やGmail連携は相変わらず1ミリもできません。
ですが、普段 arr[i] や user.age と何気なくブラケットやドットを書くとき
「こういう面倒なことを、高級言語が見えないところでよろしくやってくれてるんだな」
と、見えない恩恵に感謝できるようになりました。
高級言語さん、今日も見えないところで全部片付けてくれてありがとう!
それでは、良きGAS(GNU Assembler)ライフを!
📖 エッセイ
連載中!
ガンプラ、紡ぐ、家族
どうやら私は、ひとりだけ別の世界に住んでいるようだ (全4話)
📖 短編小説
PR記事を書かせて頂いています!
メンバーシップ始めました!
まだ手探りですが、ご要望/無茶振り/質問、可能な限りお受けします!
何でもどうぞ!答えられるものは全て答えます!
設置はお済みですか?図らずも人の庭でnote質問箱を試す!|へろん
有料ツールに挑戦中!
OBS Studio用リモコン | OBS画面を前面に出さずに操作できます!
サイトマップあります!
note活動をサポート!アプリ公開中!

いいなと思ったら応援しよう!
いつも応援ありがとうございます!
快適な制作を進めるためのおやつ活動費に使わせて頂きます!