とあるイベントでのLT。
なんでFortranせなあかんねんと。その時間を使ってね、バイナリおじさんをする。そういう考え方もできると思うんですよ。だってスパコン向けでもないのにFortran書いてもこころぴょんぴょんしないでしょ。
キミを追って何年 さかのぼったかな
キミを追って何人 失ったかな
(「怒首領蜂最大復活」テーマソング「どどんぱち大音頭」 / CAVE)
とあるイベントでのLT。
なんでFortranせなあかんねんと。その時間を使ってね、バイナリおじさんをする。そういう考え方もできると思うんですよ。だってスパコン向けでもないのにFortran書いてもこころぴょんぴょんしないでしょ。
わたしは大学で天気とか地震とか海流といった「地球物理」と呼ばれるジャンルを学んでいるのですが、このジャンルではコンピュータをガンガン回して計算しまくります。気象庁が毎日やってる天気予報もそうですし、「地球シミュレータ」でエルニーニョや地球温暖化の予測とかをやっているのもそうです。
で、そういう学科なので、コンピュータの実習があるのですが、なぜか今でもFortranを教えています。
たしかにFortranには、LAPACKのような優れた数値計算ライブラリがあったり、スパコンで書く時はコンパイラのサポートが充実してたりします。なので、そういう特定の用途には悪くない言語だと思うのですが、デスクトップPCで十分扱えるようなデータサイズで、そもそも計算速度はさして重要ではなくてデータ解析手法を学ぶ実習で使うのには辛すぎます。陽には書きたくありません。1でも書かないと単位くれないんだってさ!
なので、Fortranを書かずにFortranを書く方法を検討して、実際に実装しました。今回のソースコードも、すべてgithub上に上げています。
最初、Fortran上で小さなVMのインタプリタを実装して、そのVMの仮想機械語にコンパイルされる言語のコンパイラを実装しようと思いました。
が、VMを書くのが普通にしんどいのと、言語とコンパイラをこのためだけに実装するのもかなりつらいので、もっとラクな方法は無いか考えてみました。
冷静に考えると、別にVMのインタプリタを実装しなくとも、コンパイルされたFortranのコードは既にCPUという名前のx86機械語のインタプリタ機械(?)で実行されています。このインタプリタ(?)を利用できないでしょうか。利用できれば自力でVMのインタプリタを実装しなくていいので手間を省略できますし、コンパイラもgccとかclang/llvmとかすごいやつがいっぱい使えます!
具体的にはそのためにどうすれば良いのかというと、Fortranのinteger配列にC言語のソースをコンパイルしたx86の機械語の数字を沢山並べて入れて、そのinteger配列のポインタをC言語の関数ポインタに無理やりキャストして、そして実行すればいいのですっ!!
module a;interface;subroutine f (z) bind(c);use iso_c_binding;type(c_ptr), value :: z; end subroutine;end interface;type G;procedure(f), pointer, nopass :: x;end type;type J;integer, pointer :: x; end type;end module;program b;use iso_c_binding;use a;implicit none;type (J), pointer :: x(:); real(8),pointer :: dbl(:);type(G), pointer :: p;integer, pointer :: d(:);integer :: i;integer :: n;allocate( x(1) ); allocate( dbl(10000000) );allocate( d(10000000) );dbl = 0;x(1)%x => d(645);d(1)=-443987883;d(2)=1213580125;d(3)=267576713; d(4)=1223181585;d(5)=1223181709;d(6)=48087689;d(7)=450755289;d(8)=-128612024;d(9)=-398095544;d(10)=-532313784; d(11)=1158680562;d(12)=1438866912;d(13)=1223002440;d(14)=1223705997;d(15)=-338050423;d(16)=-1991763235; d(17)=-1958152107;d(18)=-1991708603;d(19)=267577413;d(20)=1575503120;d(21)=-1991748157;d(22)=286257893; d(23)=-1924601787;d(24)=-1991710651;d(25)=-654123582;d(26)=1209720318;d(27)=1224234377;d(28)=1223181707; d(29)=-220183159;d(30)=-532344817;d(31)=1213580125;d(32)=267576713;d(33)=1223181585;d(34)=1223181709;d(35)=48087689; d(36)=450756569;d(37)=-128612024;d(38)=-398095544;d(39)=-532313784;d(40)=1158680562;d(41)=1438866912; d(42)=-219838136;d(43)=-398126833;d(44)=-398095032;d(45)=-574453432;d(46)=-572401406;d(47)=1435060250; d(48)=1166756088;d(49)=1166625000;d(50)=269480672;d(51)=-1017257915;d(52)=-443987883;d(53)=-394426040; d(54)=-1192987255;d(55)=0;d(56)=-129660600;d(57)=16008647;d(58)=-352321536;d(59)=-196768982;d(60)=-1924622264; d(61)=50452;d(62)=-1958215680;d(63)=21555269;d(64)=269480656;d(65)=269480448;d(66)=267581517;d(67)=267567448; d(68)=-2080881391;d(69)=-1962806203;d(70)=1161557061;d(71)=1221491940;d(72)=1224230283;d(73)=-220707447; d(74)=-666562545;d(75)=1213580125;d(76)=-1991711351;d(77)=-1991710595;d(78)=1435099253;d(79)=47324;d(80)=-1991770112; d(81)=1170733125;d(82)=244;d(83)=-1958286592;d(84)=-1740049339;d(85)=-988508856;d(86)=0;d(87)=-398095544; d(88)=-221249208;d(89)=-1962405873;d(90)=-1740049339;d(91)=-988508856;d(92)=0;d(93)=-532313272;d(94)=-221249208; d(95)=-234876913;d(96)=-222209777;d(97)=-129167345;d(98)=-1051193358;d(99)=1158746098;d(100)=-196770824; d(101)=-196769023;d(102)=2094810427;d(103)=1166756018;d(104)=1166625016;d(105)=269480656;d(106)=-1017262011; (略) d(761)=-800748728;d(762)=-389576376;d(763)=-1168;d(764)=1223710091;d(765)=1220572555;d(766)=-1178057333;d(767)=20; d(768)=-389576376;d(769)=-683;d(770)=-1980742261;d(771)=535478722;d(772)=-120467455;d(773)=-223591031; d(774)=-1404753393;d(775)=-2008708280;d(776)=1118194;d(777)=16008647;d(778)=-352321536;d(779)=-196768970; d(780)=-2092394424;d(781)=-1924660800;d(782)=50452;d(783)=-1958215680;d(784)=21530693;d(785)=-196768830; d(786)=-1924622264;d(787)=50444;d(788)=-1958215680;d(789)=21545029;d(790)=9128136;d(791)=-2096985784; d(792)=-1962806203;d(793)=1161557061;d(794)=-1195213652;d(795)=0;d(796)=1213580233;d(797)=-1017256567; call c_f_pointer(C_LOC( x(1) ), p);i=0;read (*,*), n;dbl(1) = real(n);do i=2,n+1;read (*,*), dbl(i);end do;call p%x( c_loc(dbl(1)) ); n = int(dbl(1));do i=2, n+1;write (*,*), i, dbl(i);end do;deallocate ( d );deallocate ( x );deallocate ( dbl ); contains;end program;
今回はintegerの配列のポインタをCの関数へのポインタだと思わせたいため、(Cで言うと)int*をvoid (*ptr)(double*)のポインタにキャストする必要があります。しかし、Fortranは割りと型に厳しく、Cのように簡単にキャストしたり出来ません。なので、ちょっと撚(ひね)る必要があります。
!! 関数の型定義。 !! typedef void(*f)(void*); interface subroutine f (z) bind(c) use iso_c_binding type(c_ptr), value :: z end subroutine end interface !! struct FPointer { f fptr; }; type FPointer procedure(f), pointer, nopass :: fptr end type !! struct IPointer { int* iptr; }; type IPointer integer, pointer :: iptr end type
このように2つ構造体(IPointer,FPointer)を作っておき、この2つの構造体のポインタをC言語のポインタを経由することで無理やりキャストします。
type (IPointer), pointer :: ip(:); !! IPointer* ip; real(8),pointer :: dbl(:); !! double* dbl; type(FPointer), pointer :: fp !! FPointer* fp; type(C_PTR) :: cptr !! void * cptr; integer, pointer :: d(:) !命令列用の配列 !! int* d; allocate( ip(1) ) !! ip = malloc(sizeof(IPointer)); allocate( dbl(10000000) ) !! dbl = malloc(sizeof(double)*10000000); allocate( d(10000000) ) !! d = malloc(sizeof(int)*10000000); !! 命令列をセット d(1)=-443987883; d(2)=1213580125; !! 略 d(790)=-443987883; d(791)=50013; !! 645番目の要素がC言語側エントリポイント関数の頭なので、そこへのアドレスをセット。 ip(1)%iptr => d(645); !! IPointerへのポインタを、C言語のポインタ、いわばvoid*に落とす cptr = C_LOC( ip(1) ); !! そこからさらにFPointerへのポインタにキャストする(専用の関数を使います) call c_f_pointer(cptr, fp) !! 無理やり変換した関数ポインタ経由でバイナリコードにジャンプ call fp%fptr( c_loc(dbl(1)) )
IPointer 構造体へのポインタを一度Cのポインタ(いわばvoid*)にキャストし、それを再度void*からFPointerのポインタにキャストしています。構造体の中身の型が「intへのポインタ」と「関数へのポインタ」で違うので、構造体を通して、その中身についてキャストすることができました。
なんでこんな七面倒臭いことをしているかというと、intのポインタと関数のポインタを直接キャストすることができないからです。intのポインタは type(C_PTR)、関数のポインタはtype(C_FUNPTR)なので互換性がなく、さらにC言語みたいにintに無理やり落としてもう一度ポイ ンタにするような操作もできません。しかし、一回構造体でくるんでしまえばどちらも値のポインタとして扱い、キャストすることができます!
さらに、現在のLinuxでは、通常データ領域をプログラムとして実行することはできない(DEP)ので、コンパイルする時にその制限をはずす必要もあります。これには、 -z execstackというオプションスイッチが使えます。
これで、任意のx86機械語の列をFortranから実行することがきました。あとは、その機械語の列をどうやって作るかという話になりますね。原理的には自分で書いてもよいのですが、一応課題をやっているコードなので、結構複雑です。なので、C言語で書いたコードをコンパイルして、その結果を利用したいところです。
コードのコンパイルにはGCCを使うことにしましょう。
この時問題になるのは、Fortranの動的に配置した配列の上に機械語を置くので、機械語の置かれるアドレスが実行時になるまでわからないことです。なので、機械語がどこに置かれるかがわかっていることが前提の通常のコードはうまく動いてくれません。
そのようなケースは実は珍しくなく、共有ライブラリもアドレス上のどこにロードされるかは分からないので、ちゃんとそのためのコードをコンパイラが出力します2。そのようなコードのことを、PIC(Position Indepentent Code)と呼びます!gccでは、-fPICというコンパイルオプションをつけると、PICとしてコンパイルしてくれます。
じゃ、-fPICを付けてコンパイルしたバイナリから機械語をもってくれば、目的は達成できるのでしょうか…?
ところがどっこい、これではうまく行きません。なんでかというと、一個前に翻訳した記事で解説されている、PLTとGOTがあるからです。
こんな非常に簡単なC言語コードを考えてみましょう。
void calledFunction(){ } void function(){ calledFunction(); }
これをコンパイルするためのMakefileはこんな感じで、
.PHONY: all all: gcc -c -o test.o test.c -fPIC ld -shared -fPIC -o test.so test.o
コンパイルした結果が、こんな感じでーす。
test.so: file format elf64-x86-64 Disassembly of section .plt: 0000000000000280 <calledFunction@plt-0x10>: 280: ff 35 82 0d 20 00 push QWORD PTR [rip+0x200d82] # 201008 <_GLOBAL_OFFSET_TABLE_+0x8> 286: ff 25 84 0d 20 00 jmp QWORD PTR [rip+0x200d84] # 201010 <_GLOBAL_OFFSET_TABLE_+0x10> 28c: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 0000000000000290 <calledFunction@plt>: 290: ff 25 82 0d 20 00 jmp QWORD PTR [rip+0x200d82] # 201018 <_GLOBAL_OFFSET_TABLE_+0x18> 296: 68 00 00 00 00 push 0x0 29b: e9 e0 ff ff ff jmp 280 <calledFunction@plt-0x10> Disassembly of section .text: 00000000000002a0 <calledFunction>: 2a0: 55 push rbp 2a1: 48 89 e5 mov rbp,rsp 2a4: 5d pop rbp 2a5: c3 ret 00000000000002a6 <function>: 2a6: 55 push rbp 2a7: 48 89 e5 mov rbp,rsp 2aa: b8 00 00 00 00 mov eax,0x0 2af: e8 dc ff ff ff call 290 <calledFunction@plt> 2b4: 5d pop rbp 2b5: c3 ret
こんな感じで、ばっちりpltを経由して実行してしまいます。この機械語をFortran上に載せて強制的に呼び出したとしても、PLTがちゃんと解決されないとうまく動いてくれません。デフォルトのPLT解決処理は共有ライブラリとして実行されていることが前提になっているので、今回は利用することはできません。
integer配列のアドレスから関数のアドレスを解決することは原理的には可能なので、Fortran上でPLTのアドレスの所を埋めてちゃんと実行できるようにすること自体は可能(なはず)です。
しかし、かなり意味もなく複雑になってしまうので、できればPLTにジャンプするのではなくて、呼びたい関数に直接ジャンプするようなコードを出力して欲しいです。PLTを経由して関数を呼び出す時に使ってる0xe8命令は相対アドレスによるcall命令なので、可能なはずです。
リンク時にPLTを経由するようにリンクしているので、じゃあすごく単純なリンカースクリプト自分で書けばPLTみたいな便利機能使わなくなるんじゃない?
ということで、実際に書いてみました。こうなりました。
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64","elf64-x86-64") OUTPUT_ARCH(i386:x86-64) ENTRY(ep); MEMORY { ROM(rxai) : ORIGIN = 0, LENGTH = 64k } SECTIONS { .text : {} > ROM .rodata : {} > ROM .data : {} > ROM . = ALIGN(4); __bss_start = . ; .bss : {} > ROM __bss_end = . ; }
Makefileは、
linkerscript: gcc -c -o test.o test.c -fPIC ld test.so test.o --script ./linker.script
こんな感じにすると指定できます。結果は、
test-with-linkerscript: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <calledFunction>: 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 5d pop rbp 5: c3 ret 0000000000000006 <function>: 6: 55 push rbp 7: 48 89 e5 mov rbp,rsp a: b8 00 00 00 00 mov eax,0x0 f: e8 ec ff ff ff call 0 <calledFunction> 14: 5d pop rbp 15: c3 ret
となって、見事、PLTを使わずに直接相対ジャンプするようになりました!
実をいうと、リンカースクリプトなんて書かなくても、-pieというオプションを付けて、通常の実行ファイルとしてコンパイルすればplt/gotを使わないコードを生成してくれます。
pie: gcc -c -o test.o test.c -pie ld -pie test.so test.o
-pieというオプションは、gcc –helpすると、
-pie Create a position independent executable
ということで、実行時にどのアドレスに配置されても実行できる実行ファイルを生成してくれます。
実行ファイルは共有オブジェクトとは違って外部の他のプログラムからその中の関数が呼ばれるようなことはないので、内部のそれぞれの関数のアドレスを動的に解決する必要はありません。なので、PLTを経由せずに直接callするようなコードにしてくれる…のでしょう。
…でも冷静に考えると、共有ライブラリの中でならPLT経由せずに直接ジャンプでよくない?
全てが終わったあとに気づいたので時既に遅しという感じでしたが、一応書いておきます。
残るところは、出来上がったファイルから機械語を抽出して、数字の列にするだけです。そのために、機械語が書いてある部分はファイルのなかでどこなのかを調べましょう。
セクションの一覧を出力するには、objdump -hが使えます。
% objdump -h test.so test.so: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 0 .interp 0000000f 00000000000001c8 00000000000001c8 000001c8 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .hash 00000028 00000000000001d8 00000000000001d8 000001d8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .dynsym 00000078 0000000000000200 0000000000000200 00000200 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .dynstr 00000019 0000000000000278 0000000000000278 00000278 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .text 00000016 0000000000000294 0000000000000294 00000294 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 5 .eh_frame 00000058 00000000000002b0 00000000000002b0 000002b0 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .dynamic 000000c0 0000000000200f40 0000000000200f40 00000f40 2**3 CONTENTS, ALLOC, LOAD, DATA 7 .comment 0000002c 0000000000000000 0000000000000000 00001000 2**0 CONTENTS, READONLY
機械語本体が置かれているのは、.textというセクションです。この中のデータをFortranに書きましょう。File offの項目が、ファイルの先頭からの位置で、Sizeという項目がセクションのサイズで、今回の場合、要はプログラムコード全体のサイズです。
さらに、関数呼び出しするには、呼び出したい関数の、プログラムコード内での相対位置も必要です。これには、nmコマンドが使えます。
% nm test.so 0000000000201000 D __bss_start 0000000000000294 T calledFunction 0000000000200f40 d _DYNAMIC 0000000000201000 D _edata 0000000000201000 D _end 000000000000029a T function 0000000000000000 d _GLOBAL_OFFSET_TABLE_ U _start
セクションの所のVMAもしくはLMAというのがメモリ上での位置なので、機械語列の中での相対位置を調べるにはここでのアドレスから.textセクションのVMA/LMAの値を引けば求めることができます。
シェルスクリプトを使うともちろんこの作業は自動化できます。呼び出す関数名は”ep”としました。EntryPointの略ですね!
_start=$(objdump -h test.so | grep \\s.text\\s | awk '{ print $6 }') _size=$(objdump -h test.so | grep \\s.text\\s | awk '{ print $3 }') _ep=$(nm test.so | grep \\sep$ | awk '{ print $1 }')
Cのソースをコンパイルする所から、命令列を抽出してFortranのソースに変換する作業まで、これらの作業はすべて自動化しているので、よかったらgithubのソースを見てね!
今回書き込んだinteger型は4バイトなので、エントリポイントのオフセットが4の倍数でないとき(よくある)には、その整列条件を満たすために数バイトズラしたりしないといけません。そのコードとかも入ってます。
これで終わり…ではありません!もう一個、プログラムを書くという問題があります!!今回の環境では、
という、非常に大きな制限があります。実行時に決定されるライブラリ関数の実際のアドレスを知らないし、知るためにはライブラリ関数を呼び出さないといけないので、「服を買う服がない」状態なんです。もちろん、Fotranでなら調べられるので、調べた結果をC言語の関数に投げればいいんですけど、Fortran書きたくないし(ぉ)。
科学計算すればいいだけなので、sin/cosとsqrtくらいが使えればなんとかなります。sin/cos/sqrt/absは、FPU命令を使って計算できるので、ライブラリは必要ありません。
そのためには、インラインアセンブリで陽にFPU命令を実行する必要があります。
double fsin(double v) { intptr_t d0; __asm__ ( ".intel_syntax noprefix;" "fld qword ptr [%0];" "fsin;" "fstp qword ptr [%0];" ".att_syntax;" : "=&r"(d0) : "0"((intptr_t) &v) : "eax", "ecx" ); return v; }
こんな感じのを、FPUの命令ごとに実装していきます。
関数だけでなく、piもFPUを使って取得しています。なんでかというと、今回は機械語だけを埋め込んで、それとは別にある定数の領域をFortranのソースに含めていないからです!
Cのソースに円周率の値を直接書いていてもすこし複雑な処理になると定数領域に飛ばされてしまって、.textセクションに含まれなくなってしまい、結果としてプログラムがおかしくなってしまいました。定数領域もFortranに埋め込むだけでその問題は回避できるのですが、その改修がちょっとめんどくさかったのでやめて、FPU命令を毎回呼ぶことで回避しました。
入力と出力ですが、実際のデータの読み込みと結果の出力のみ、Fortranが担当することにしました。C言語は関数のシグネチャをvoid (*fun)(double *)とし、doubleの配列を処理してその結果をもとの配列の領域に書き込むことだけに専念していただきます。
と思った。Fortranはなんだかんだ任意の機械語を実行する難易度は低い言語だったと思います。
なんでFortranせなあかんねんと。その時間を使ってね、バイナリおじさんをする。そういう考え方もできると思うんですよ。だってスパコン向けでもないのにFortran書いてもこころぴょんぴょんしないでしょ。
— ψ(プサイ) (@tikal) June 14, 2014
書かなくて良くなった(╹◡╹)
高校宜しく職員室ならぬ教員室で面談したらFotran以外で書いてもOKって言われたよーやったー(╹◡╹) #プログラミング言語選択の自由は基本的人権
— ψ(プサイ) (@tikal) June 3, 2014
ひさびさに色々バイナリが触れてたのしかったです。まぁ結果としては良かったのではないでしょうか!
工学部「Fortran?書いたこと無いですね…」 文系「Fortranって何?」 理学部「(他にもっとうまい人いるしパソコンは計算機としか思ってないし)Fortranあんまり出来ないですね…」 わたし「(機械語をソースに直接書いて実行できるから)Fortranできます!!!!」
— ψ(プサイ) (@tikal) June 4, 2014
「ローレイヤー勉強会」というイベントでのLT。
以前、フリーのSTGゲーム“GENETOS”の音楽を復号化する“Oreshiki Decrypter”を公開する時に書いた、GENETOSのBGMを差し替えるための改造パッチを公開する…と言いながら忘れていましたが、最近ソースコードのフォルダを眺めていたら発見されたので配布します。
け、結構前…。
ダウンロードして出てくる「_patch_genetos.EXE」を実行するとパッチが適用されます。
次に、BGMのファイルを次のように書き換えてください。
内部では音楽を再生する関数はmidiを再生するものとMP3を再生する関数に別れています。どちらもファイル名と追加のいくつかの引数を取るもので、これを利用して、midiを再生する関数を書き換えてmp3を再生する関数にバイパスしています。
ただしひとつ問題があって、midiを再生する関数は、ステージ開始時に一気にmidiをロードしてしかるタイミングで再生するBGMを変更する、という事ができるのですが、mp3の関数ではそれはできませんでした。この問題を解決すべくいろいろ試行錯誤した結果、自機が進化する瞬間の処理をフックしてMP3再生関数を呼んでBGMを切り替えています。
メニューから普通にゲームを開始する時以外の動作に関してはうまく動かないと思われます。フリープレイでちゃんと動く事とかは考えてません。
「アフターキャンプ」というイベントでやったLT。
slideshareからこちらにコピー。クラウド。それは、現代の監獄。
今日はひたすらアセンブラを追いかけるだけのお話です。ごめんね。
C++では定数の定義を、defineでなく、static constな変数で行うことが推奨されてるらしいです。
// これはC++では(・A・)イクナイ!! #define VAL (1) //こっち推奨 const int VAL = 1;
基本的には、これで問題ないのですが、リンク先によると、この定数がクラス変数の場合、定義と実体の二つに分けないといけないそうです。
//ヘッダファイル側(宣言) class Test { private: protected: public: static const int VAL=01234; }; //ソースファイル側(実体) //gccでは宣言(ヘッダ)に値を書いても良いけど、VC++等の古いコンパイラだと実体に書かないといけない。 //そのためにenumハックが存在する(上記のリンク先参照) const int Test:VAL;
ほえー、なるほど。
が、しかし。実際には、gccのときは、実体を書かなくても割と大丈夫みたいです。Ubuntu11.04のx86-64のgccで実験してみました(Win32のMingwでも、機械語は違いますが同じ結果でした)。
以下のソースはgithubからもDLできるよ☆彡 あ、ZIPももちろんあるよ!!
#ifndef TEST_H #define TEST_H class Test { private: protected: public: static const int VAL1=1234; static const int VAL2=4321; }; #endif
特に意味はないです。このTestというクラスの定数を使って、いろいろテストしてみましょう。
以下test*-*とありますが、これをmakeのビルドターゲットにすると自動でコンパイルとかしてくれます。
make test*-*
#include "./Test.h" #include <iostream> int main(int argc, char** argv) { std::cout << "VAL: " << std::hex << Test::VAL1 << std::endl; return 0; }
ヘッダ上のTest::VAL1やTest::VAL2の実体を一切定義していないことに注意していください。
これをコンパイルして実行すると…
% g++ -o Test1-1.out Test1-1.cc % ./Test1-1.out VAL: 1234
どうなってるの!?と思って、アセンブラを出力させてみると…
% g++ -S -masm=intel Test1.cc ←こうすると見慣れたINTEL形式で出力してくれます % cat Test1.s
main: .LFB963: .cfi_startproc push rbp .cfi_def_cfa_offset 16 mov rbp, rsp .cfi_offset 6, -16 .cfi_def_cfa_register 6 sub rsp, 16 mov DWORD PTR [rbp-4], edi mov QWORD PTR [rbp-16], rsi mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:_ZSt4cout call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ;--------------------------- mov esi, 1234 ;--------------------------- mov rdi, rax call _ZNSolsEi mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ mov rdi, rax call _ZNSolsEPFRSoS_E mov eax, 0 leave .cfi_def_cfa 7, 8 ret .cfi_endproc
線で囲ったところを見てもらえばわかるとおり、インライン展開されています。ちなみに、1234という数値はこの場所以外一切定義されません。
定義を書き加えて
//ヘッダファイルとメイン関数の間に追加。 const int Test::VAL1;
としてアセンブラを出力してみると…。
;実体も定義される _ZN4Test4VAL1E: .long 1234 ;(略) main: .LFB963: .cfi_startproc push rbp .cfi_def_cfa_offset 16 mov rbp, rsp .cfi_offset 6, -16 .cfi_def_cfa_register 6 sub rsp, 16 mov DWORD PTR [rbp-4], edi mov QWORD PTR [rbp-16], rsi mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:_ZSt4cout call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ;--------------------------- mov esi, 1234 ;--------------------------- mov rdi, rax call _ZNSolsEi mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ mov rdi, rax call _ZNSolsEPFRSoS_E mov eax, 0 leave .cfi_def_cfa 7, 8 ret .cfi_endproc
Test::VALの値が埋め込みになるのは同じでしたが、それとは独立した実体も定義されました。
一切最適化オプションをつけなくても、それなりに最適化してくれるみたいです。
ポインタを通す場合、アセンブラに直接引数を書くことはできません。メモリ上に一旦置かなければなりません。
#include "./Test.h" #include <iostream> void outVal(const int* val) { std::cout << "VAL: " << *val << std::endl; } int main(int argc, char** argv) { outVal(&Test::VAL1); return 0; }
今度からは面倒なのでビルドターゲットでコンパイルします。†1
% make test1-3 g++ -S -masm=intel Test1-3.cc -o Test1-3.s g++ -o Test1-3.out Test1-3.cc /tmp/ccIrdzjI.o: In function `main': Test1-3.cc:(.text+0x50): undefined reference to `Test::VAL1' collect2: ld returned 1 exit status make: *** [test1-3] エラー 1
案の定実行は失敗ですね。アセンブラコードを見てみると?
;実体は定義されないけど... ;(略) main: .LFB964: .cfi_startproc push rbp .cfi_def_cfa_offset 16 mov rbp, rsp .cfi_offset 6, -16 .cfi_def_cfa_register 6 sub rsp, 16 mov DWORD PTR [rbp-4], edi mov QWORD PTR [rbp-16], rsi ;--------------------------- mov edi, OFFSET FLAT:_ZN4Test4VAL1E ;使おうとする ;--------------------------- call _Z6outValPKi mov eax, 0 leave .cfi_def_cfa 7, 8 ret .cfi_endproc
予想通りですね。
でもしかし。同じソースのまま、-O3とかを付けて最適化を掛けると、実行可能です。
make test1-4 g++ -S -masm=intel Test1-3.cc -o Test1-4.s -O3 ;ソースは上と同じTest1-3.cc g++ -o Test1-4.out Test1-3.cc -O3 ./Test1-4.out VAL: 1234
main: .LFB1004: .cfi_startproc push rbp .cfi_def_cfa_offset 16 mov edx, 5 mov esi, OFFSET FLAT:.LC0 ;std::coutへの出力を行う別の関数(outVal)があったのに、インライン展開されてる。 ;(展開されてない、outValの実体も別箇所で存在します。) mov edi, OFFSET FLAT:_ZSt4cout push rbx .cfi_def_cfa_offset 24 sub rsp, 8 .cfi_def_cfa_offset 32 .cfi_offset 3, -24 .cfi_offset 6, -16 call _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l ;---------------------------------- mov esi, 1234 ;ハードコーディング。 ;---------------------------------- mov edi, OFFSET FLAT:_ZSt4cout call _ZNSolsEi mov rbx, rax mov rax, QWORD PTR [rax] mov rax, QWORD PTR [rax-24] mov rbp, QWORD PTR [rbx+240+rax] test rbp, rbp je .L11 cmp BYTE PTR [rbp+56], 0 je .L9 movzx eax, BYTE PTR [rbp+67]
ポインタとして使えないのはdefineでも一緒ですし、だったら同じ条件ですねやったー。
…と言いたいところだが、(大佐風)微妙にこったことをすると必ず実体の定義が要らなくなるとは限りません。
三項演算子を使って、フラグによって定数を振り分けたい!ありそうなシチュエーションです。
#include "./Test.h" #include <iostream> int main(int argc, char** argv) { int val = argc > 1 ? Test::VAL1 : Test::VAL2; std::cout << "VAL: " << val << std::endl; return 0; }
これを最適化なしでコンパイルすると…
% make test2-1 g++ -S -masm=intel Test2.cc -o Test2-1.s g++ -o Test2-1.out Test2.cc /tmp/cc87c0Ta.o: In function `main': Test2.cc:(.text+0x17): undefined reference to `Test::VAL1' Test2.cc:(.text+0x1f): undefined reference to `Test::VAL2' collect2: ld returned 1 exit status make: *** [test2-1] エラー 1
なんと。駄目でした。アセンブラを読んでみると、
;もちろん実体は定義されてない。 main: .LFB963: .cfi_startproc push rbp .cfi_def_cfa_offset 16 mov rbp, rsp .cfi_offset 6, -16 .cfi_def_cfa_register 6 sub rsp, 32 mov DWORD PTR [rbp-20], edi mov QWORD PTR [rbp-32], rsi ;---------------------------------- ここから三項演算子 cmp DWORD PTR [rbp-20], 1 jle .L2 mov eax, DWORD PTR _ZN4Test4VAL1E[rip] ;実体は無いのに jmp .L3 .L2: mov eax, DWORD PTR _ZN4Test4VAL2E[rip] ;使おうとする。 ;---------------------------------- .L3: mov DWORD PTR [rbp-4], eax mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:_ZSt4cout call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc mov edx, DWORD PTR [rbp-4] mov esi, edx mov rdi, rax call _ZNSolsEi mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ mov rdi, rax call _ZNSolsEPFRSoS_E mov eax, 0 leave .cfi_def_cfa 7, 8 ret .cfi_endproc
でも最適化をつけると…
make test2-2 g++ -S -masm=intel Test2.cc -O3 -o Test2-2.s g++ -o Test2-2.out Test2.cc -O3 ./Test2-2.out VAL: 4321
main: .LFB1003: .cfi_startproc push rbp .cfi_def_cfa_offset 16 mov eax, 4321 mov edx, 5 mov esi, OFFSET FLAT:.LC0 push rbx .cfi_def_cfa_offset 24 mov ebx, 1234 .cfi_offset 3, -24 .cfi_offset 6, -16 sub rsp, 8 .cfi_def_cfa_offset 32 cmp edi, 2 mov edi, OFFSET FLAT:_ZSt4cout ;---------------------- cmovl ebx, eax ;フラグを見て、条件にあうならeaxをebxへ。ここが三項演算子。 ;---------------------- call _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l mov esi, ebx mov edi, OFFSET FLAT:_ZSt4cout call _ZNSolsEi mov rbx, rax mov rax, QWORD PTR [rax] mov rax, QWORD PTR [rax-24] mov rbp, QWORD PTR [rbx+240+rax] test rbp, rbp je .L8 cmp BYTE PTR [rbp+56], 0 je .L4 movzx eax, BYTE PTR [rbp+67]
まさかの分岐命令…なし…!?cmovlはこちら参考。
また、何度もstd::coutの関数を呼ぶのでなく、出力関数は一度だけのようです。
じゃあ、if文で同じことをしましょう。
#include "./Test.h" #include <iostream> int main(int argc, char** argv) { int val; if(argc > 1){ val = Test::VAL1; }else{ val = Test::VAL2; } std::cout << "VAL: " << val << std::endl; return 0; }
% make test2-3 g++ -S -masm=intel Test2-3.cc -o Test2-3.s g++ -o Test2-3.out Test2-3.cc ./Test2-3.out VAL: 4321
むむ。成功してしまいました。アセンブラは?
main: .LFB963: .cfi_startproc push rbp .cfi_def_cfa_offset 16 mov rbp, rsp .cfi_offset 6, -16 .cfi_def_cfa_register 6 sub rsp, 32 mov DWORD PTR [rbp-20], edi mov QWORD PTR [rbp-32], rsi cmp DWORD PTR [rbp-20], 1 jle .L2 mov DWORD PTR [rbp-4], 1234 jmp .L3 .L2: mov DWORD PTR [rbp-4], 4321 .L3: mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:_ZSt4cout call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc mov edx, DWORD PTR [rbp-4] mov esi, edx mov rdi, rax call _ZNSolsEi mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ mov rdi, rax call _ZNSolsEPFRSoS_E mov eax, 0 leave .cfi_def_cfa 7, 8 ret .cfi_endproc
分岐命令で割り振るまでは三項演算子と同じ。でもif文の場合はインラインで定数を書いてくれるみたいですね。
ちなみに、最適化をかけたときの結果は、三項演算子の時と殆ど同じになりました(test2-4)。
残りのテストの結果は上記の結果から推測できる面白くない結果だったので概要だけ。
どうやら、三項演算子はif文に展開されたりするわけではなく、別扱いになっていて、最適化フラグが掛からないときは扱いが少し違う…そんな感じでしょうか。gccのソースは読んでないのでわかりませんが…。
エミュレータで実体を定義し忘れてたのになぜか動いていたのですが、三項演算子を使ったところでコンパイルエラーでした。なお、実体を定義する前にVC++でビルドしてたので、VC++も同じようにやってくれるみたいですが、最適化の結果どうなるのか、とかは確かめてないです。
ほんとC++は黒魔術やでぇ…いえ、仕様通り書かない私が悪いんです。でも動いちゃうのもちょっと困るよ…。
なにやら物騒なタイトルで。たまにはタイトル一本釣り。
リア充の皆様。AndroidにiPhoneといったスマートフォンを活用してリアルを充実させていらっしゃることと存じます。今回、私どもは、皆様のスマートフォンからTwitterへ送信することができる位置情報から、住所を特定できるのでは無いか、というありがちだけど実際検証した人は居ない†1仮説に基づき、実際にそれを行えるのかどうかやってみました。
今回開発したアプリでは、過去のツイートのうち、位置情報を持っている物を抜き出してGoogleMaps上に配置します。このアプリでは自分の位置情報のみを取得するようにしていますが、誰でも取得できる情報です。
こちらからアクセスしてください。
この画面で「Twitter経由でログインする!」をクリックすると、このようにOAuthの認証画面が現れます。ログインを押して続行できます。
ログインに成功すると、このような画面になりますので、「ロード」を押してツイートの読み込みを開始してください。
うまく読み込むと、こんな感じで地図上にマッピングされます。このψ(プサイ)という人は関東から…出ていないようです…w
ズームしていくと…つくばエクスプレスユーザーだと一目で分かってしまいますーー;;
自宅周辺では、たくさんのピンが立っているのが特徴なのはもちろん、こういった自宅でしてるっぽいツイートがあるのも特徴です。
単に位置情報だけでは通勤・通学で使う駅等で呟いてることが案外多かたりで、それだけで住所まで絞り込むのは難しいです。
ですので、発言内容も手がかりにしてみてください。住所だけじゃなくてよく使うレストランとかも特定できます←父親のアカウントで検証済み、こがねちゃん弁当乙
色々と生活パターンが分かって怖い…^^;こち亀でGPSの大きな装置を背負って生活してもらう!なんて話が100巻~120巻くらいにありましたけど、それが現実になってるんだな~、って感じです。もちろん、背負わなきゃいけないほど大きな装置ではありあませんが。
Twitterの設定画面から、あなたの位置情報をすべて消すことができます。
書かれている通り、大体30分くらい時間が必要なようです。何か怖いと感じたら消してしまって良いと思います。
位置情報って活用されてるんでしょうか?私はなんとな~くiPhoneの時は毎回入れていましたけど、冷静に考えるとあんまりいらない機能である気がして仕方がありません…w
政情不安とか地震とかの超リアルタイム緊急時だったら案外活用されてるのかな。
sessionでOAuth用のtokenの受け渡しとかが出来るのが便利~。やっぱりフレームワーク使うとサクサクできていいですね~。
ふと思いついたネタを。単純なネタですがこの点に関する注意をしている記事を見た記憶がないので。
scaffoldは気をつけて使わないと、ユーザから上書きされてはいけないフィールドが書き換えられる恐れあり。scaffoldは便利だけど、そのままではすこし危険。
とりあえずRuby on Railsが有名だと思うのでRuby on Rails(バージョン2.2.3)で書きますが、grailsでも同様の問題がありました。
さて、まずはscaffoldしないと話になりません:
% rails test % cd test % script/generate scaffold User name:string password:string is_admin:boolean
としてモデル・コントローラ・ビューを自動生成できますよね。このうち、おそらく管理者かどうかをチェックするためにつけたと思われる「is_admin」が、そのままだと一般ユーザから書き換え放題になります。
どうやるかって?簡単です。newアクションのviewはこうなりますよね。
この時の、生成されるhtmlのうち:
<p> <label for="user_is_admin">Is admin</label><br /> <input id="user_is_admin" name="user[is_admin]" type="checkbox" value="1" /><input name="user[is_admin]" type="hidden" value="0" /> </p>
のチェックボックスの<input>とその後のhiddenな<input>が並んだところをコピーして控えておきます。この部分は、このアプリだけでなく他のアプリを攻撃する際にも使い回せます。(name属性だけ書き換えて再利用します。)
さて。実際のアプリにおいて、is_adminは管理者が設定するフラグであって、ユーザ側から選択できてはマズいですから、削除すると思います。こんな感じで:
<h1>New user</h1> <% form_for(@user) do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <p> <%= f.label :password %><br /> <%= f.text_field :password %> </p> //-------------ここから削除------------------ <p> <%= f.label :is_admin %><br /> <%= f.check_box :is_admin %> </p> //-------------ここまで削除------------------ <p> <%= f.submit "Create" %> </p> <% end %> <%= link_to 'Back', users_path %>
そうするとこんな感じの画面になって、一見ユーザからは管理人であるかどうかは選択できなくなりますよね:
実際に、このまま普通に作っても、「Is admin: 」となって、falseになっていることが分かります:
ところがどっこい、動的にHTMLを書き換えてさっきメモした<input>のコードを動的にフォームの中にコピーすると管理者になれちゃいます!w
FirefoxのFirebugや、Chromeのデベロッパーツールを使えば簡単にHTMLを動的に書き換えることできます。プラグインをインストールする必要が無くて楽なChromeで試してみます。
デベロッパーメニューはこちらにあります†1:
次に、動的に書き換えるためにformのところをクリックして、「Edit as HTML」を選びます:
すると編集できるようになるので、先程の<input>のコードを<form>のすぐ次くらいにでも張り付けます:
これで終わりです。適当にエディット領域外をクリックして編集を終了すると…
チェックボックスが出てきます。あとはご想像のとおり。これを黒くチェックして、適当にIDとパスワードを入力すると…
はい、管理者ユーザになれました。
まあたしかに、is_adminという名前が分からないと書き換えはできないので、ソースコードを公開してないアプリなら即危ないというわけではありませんが、セキュリティ的にはやっぱりマズイと思います。特にオープンソースアプリ…大丈夫でしょうか?
Userコントローラのcreateアクションのうち、
@user = User.new(params[:user])
この部分を、
@user = User.new(params[:user]) @user.is_admin = false
にするとか(今確認したらredmineだとこうしてる)、
@user = User.new(:name=> params[:user][:name], :password=> params[:user][:password])
として、そのアクションで何を書き換えるのかをはっきりさせる、とかでしょうか。
すごく単純なミスですが、実を言うと今まで全く気づかなかったので今回掲載しました^^;UFO大好き霊子さんにもこのミスがあります。証明写真作成工房は修正しておきました。
出力されるformの中身を自動的にチェックして、formの中身以外のがリクエストに混入してたらアウトって出来ないのかなあ?その方がrailsっぽいよね。Ajaxで動的にフォームを生成するときは、そのための設定をするとか。
ソースコード読んだら修正できるだろうか?
全自動無限ルイズコピペ生成器搭載!
Linux2.6(32bit/64bit)用デバイスドライバです(2.6.32で動作確認しています)。インストールすると/dev/louise_loveが現れ、読み出すとルイズコピペをマルコフ連鎖アルゴリズムに基づいて無限に自動生成し続けます。
ちなみに/dev/louise_loveへ書き込むとすべて書き込みを読み捨てますので、自分の書いたポエムも安心して読んでもらえます。
まずはダウンロードして解凍し、フォルダを移動しましょう。
% git clone https://github.com/ledyba/louise_love_driver.git % cd louise_love_driver/driver
コンパイルするには、普通のプログラムのようにmakeしてください。
% make
デバイスドライバドライバと同時に、ユーザランドで動くアプリケーションとしてもコンパイルされているので、いきなりデバイスドライバとして組み込むのが不安な方は、安全なユーザランドで試すことができます。それには、
% make test もしくは % ./userland_louise_love
として実行してください。
では、実際にデバイスドライバとしてインストールしてみましょう。root権限が必ず必要です。
% sudo make install
おめてとうございます、インストールが完了しました。ルイズたんへの愛を得るにはこのようにしてください。
% cat /dev/louise_love
ゆっくりルイズたんへの愛を噛み締めたい場合は、moreコマンドなどを使うと良いでしょう。
ハードディスクやSSD、USBメモリをルイズたんへの愛で埋める場合には、次のようにしてください(注意:これをやるとHDDのデータが壊れます)。
% dd if=/dev/louise_love of=/dev/sda
/dev/louiseのパーミッションは666なので、書き込みも行うことができます。書き込んだデータはすべてスルーしますので、恥ずかしいポエムも安心して書き込むことができます。
% cat my_poem.txt > /dev/louise_love
昨今はアニメが多いですから、ルイズたんへの100年の恋も冷めてしまうかもしれません(一期やってたのもう4年くらい前だし)。でも大丈夫、アンインストールは次のように簡単にできます。
(インストール時に解凍したフォルダで) % sudo make uninstall
簡単なソースではあるんで、デバドラやマルコフ連鎖の超簡単なサンプルとしてご活用ください。適当に書いたのでソースコードはむちゃくちゃ汚いですがw なお、/dev/yoshinoyaなども(つくろうと思えば)簡単にできます。
任意の文章に置き換えるには、ユーティリティをダウンロードして、
% ruby make_c_source.rb <文章ファイル> > c_source.txt
として出力結果のc_source.txtの中身を、louise_love.cの該当部分に適当に貼りつけてください。
ちなみに、
% ruby markov.rb <文章ファイル>
とするとrubyを使っていくつか文章を作ってくれます。
あまりカーネルランドをいじった感が無いのがちょっと…。8割のコードはAPIの名前さえ変えればユーザランドでも動きますし、そもそもデバイスドライバなのに、ハードウェアを触っていません。
ユーザランド時
% ./userland_louise_love | dd of=/dev/null 1130544+0 記録始め 1130544+0 記録終わり 578838528 バイト (579 MB) コピー終了, 25.5481 s, 22.7 MB/s
カーネルランド時†1
% cat /dev/louise_love | dd of=/dev/null 3619712+0 記録始め 3619712+0 記録終わり 1853292544 バイト (1.9 GB) コピー終了, 25.9583 s, 71.4 MB/s
単純比較していいのかわからないけどさすがカーネルランド3倍はえーw
独立行政法人情報処理推進機構(IPA)の主催する「プログラミング&セキュリティキャンプ2010」の「セキュリティコース/ソフトウェアセキュリティ組」への参加が決定したのでお知らせします。
てめーらの税金使って遊んでくる†1からwwwwwwwうへえwwwwwwww
応募用紙に「社会の役に立ちたいといった目的はありません、純然たる知識欲、あるいはハッカーになりたいという思いから応募しました」とはっきり書いた上で選考されたのだから多分問題ないと思います(キリッ
高校の時から参加したかったんですが受験で参加できず、大学入ってからやっとこさの参加でございます。浮かないかなあ?
税金で思い出しましたが、大学のMacでアニメ見るとなかなか画面も大きくて迫力があって良いですよ!!なぜ誰も見ていない!!最近は忙しくてなかなか出来ないのですが…orz
9月にも試験があると書いたばかり…なんか自分で自分の首を絞めている気がする(
小学校の同窓会と思いっきり被ってるんですが気にしない事にしました(キリッ
結局ネットがあるし、同窓会っても本当に8年ぶりに会う人って稀なんだよなあ。