作者azureblaze (AzureBlaze)
看板C_and_CPP
標題Re: [情報] C++大師認證 (PA9)
時間Thu Feb 27 00:53:38 2014
昨天剛弄完PA9,分享一下心得
首先,去你的X86/64!
多年的包袱加上CISC架構讓他複雜的要命
如果毫無準備就翻開MOV那頁絕對會一頭撞上鐵板
所以以下是書單
http://www.intel.com/content/www/us/en/processors/
architectures-software-developer-manuals.html
Volume1
3.6 OPERAND-SIZE AND ADDRESS-SIZE ATTRIBUTES
3.7 OPERAND ADDRESSING
8.1.2 X87 FPU EXECUTION ENVIRONMENT
8.1.4 Branching and Conditional Moves on Condition Codes
8.2 X87 FPU DATA TYPES
Volume2
2.1 Instruction Format
2.2 IA-32e Mod
3.1 INTERPRETING THE INSTRUCTION REFERENCE PAGES
其中 V2的2和3章特別重要
== X86/64 machine code的基本組成 ==
CPPGM應該全部都會在64bit mode下跑,可是手冊都先寫32bit然後在插入
一個段落寫到了64下會怎樣,讀起來頭有點痛...
一個instruciton的結構是這樣
[ Prefix ] [ REX ] [ Opcode ] [ ModR/M ] [ SIB ] [ Displacement ] [ Immediate ]
(Opt) (Opt) ( 如果Opcode或ModR/M需要 )
[Prefix]
有好幾種,可是會用到的應該只有 0x66 (Operand-size override)
同樣一個指令通常有對應不同大小Operand的版本
一般8bit會有獨立的Opcode,而16/32/64會共用Opcode.
預設的Operand大小是32bit
如果要改成作用在16bit上就必須在最前面掛一個0x66,這點手冊在後面的指令說明不
會額外標出來。64bit Operand則是加在後面的REX上
[REX]
REX是為了對應64bit下的64bit Operand、R8~R15 register、額外定址模式而新增的修飾
如果一個指令用到上述的東西就需要輸出這個byte
REX有四個欄位:
REX.W 把Operand大小改成64bit
REX.R 用來擴增ModR/M的reg,使其可以代表R8~R15
REX.X 用來擴增SIB的index,應該用不到
REX.B 用來擴增ModR/M的r/m,如果用到SIB則改為擴增SIB的base
一樣讓其可以選擇R8~R15
因為CY86建議的register對應會用到R8以上,所以REX應該常常用到
[Opcode]
各指令的Opcode
比較特別的是如果指令的形式是 DB D0+i
那就要把i的值直接加到D0上,手冊會說明i代表的是什麼
如果是 0F 5F /r
那就代表這個指令用到ModR/M而且他的reg是一個register
如果是 0F 5f /3 這種形式
代表ModR/M的reg不代表register而是一個一個常數,
讓不同的指令共用opcode再用reg分別
[ModR/M]
這大概是最難搞的東西。
ModR/M用來指定1~2個operand的值,
可以代表一個register,和一個register或是register內位址
ModR/M分為三個部份
{ mod } { reg } { r/m }
2bit 3bit 3bit
{mod}
代表後面的{r/m}的意義
{reg}
一個register,或是前面所說得opcode常數
register的編碼方式為
RAX RCX RDX RBX RSP RBP RSI RDI
R8 R9 R10 R11 R12 R13 R14 R15
0 1 2 3 4 5 6 7
如果要用R8~R15那就必須把前面REX.R設為1
{r/m}
r/m通常和reg一樣用來選擇一個register,不過register處理的方式會隨mod變化
當要選擇R8~R15時要把REX.B設為1
當mod為11時
r/m 代表另外一個register,
當mod為00時
r/m 代表這個register內的記憶體位址
當mod為01或10時
r/m 代表這個register內的記憶體位址加上一個offset
offset為有號數,mod為01時是8bit,10時是32bit
放在後面的Displacement裡
注意的是如果mod不為11,而r/m為100時
Mod/RM會改成啟動SIB模式
因此如果要取RSP或R12內的記憶體就必須加上SIB
預設的cy86 x register會碰到這個問題
另外當mod為00而r/m為101時
ModR/M會變成直接記憶體位址模式,
因此如果要取RBP或R13的值,就需改成用
mod= 01 or 10 的offset模式,並將offset設成0
[SIB]
當mod不為11且r/m為100時啟動
改成用Scale-Index-Base的方式計算r/m的位址
目前的用途用0x20 + r/m當值就夠了
(scale = 1, index = none , base = r/m,
addr = [base] + [index] * scale = [base] )
[Displacement]
ModR/M用到Displacement,或是指令指定的時候需要
[Immediate]
指令指定的時候需要
------------------------------------------------
所以以
move64 x64 [sp - 8]
為例
翻譯成asm是
mov R12, QWORD PTR[sp - 8]
查表得到的是
REX.W + 8B /r MOV r64,r/m64
把 r/m64位址內的內容搬到 r64裡
首先指令大小是64bit,所以REX.W為1
R12是擴充register,所以REX.R為1
operand是register和register內的位址加8bit offset,所以mod為01
很不幸的RSP對應的r/m是 100,因此後面得再掛上SIB
ModR/M = 01 100 100 = 0x64
mod reg SIB
SIB [RSP] + 1*none 對應的值是 0x24 (Vol2,2.1.5有表)
SIB = 00 100 100 = 0x24
S=1 * I=None + Base=RSP
到此為止REX.X和REX.B沒用到,因此為0
REX的值為 01001100 = 0x4C
WRXB
Displacement是8bit -8,因此值為 F8
Immediate沒用到,忽略
所以整個指令為
4C 8B 64 24 F8
REX Opcode ModRM SIB Displacement
再跟我說一遍
去 你 的 X86/64!
== 個別test注意事項 ==
100-noop
100-ret42
這兩個必須想辦法看懂怎麼把immediate搬進register裡
過了之後就可以用return value撈裡頭的資料來debug,雖然只能撈1byte
110-hello-world
注意的是CY86假設所有的資料和程式都和source的順序一樣,
如果你和我一樣亂搬這裡可能會出問題
會被資料順序影響的也只有這裡和後面的error output(正常不會出現)
200-duplicator
mov bug爆發
210-reverser
指令用的和前面差不多所以前面過了這個應該也會一起過
這兩個過了之後也可以確定test的共用header可以正常使用
建議可以把程式輸出pipe到檔案
然後再write(t64,stdout,data,size),最後把檔案hexdump
可以檢查記憶體內容對debug非常有幫助
220-hexdump
300-binary-calculator
mov bug大爆發
400-integer-calculator
mov bug爆發完畢
這個之後應該就可以安心使用改寫好幾遍的mov了
500-to-float80
501-from-float80
開始處理FPU
FPU的operend都是對記憶體,因此把東西先存到rsp-16之類的比較好處理
注意的是unsigned沒辦法直接讀進FPU裡
8 16 32bit的可以轉成更大型態處理
64bit就只能靠bit magic了
u64 -> f80
如果大小小於0x8000 0000 0000 0000 當成signed處理就好了
f80的fraction剛好是64bit,把他copy過去
sign是0,exponent是 16383(expoffset) + 64(把fraction推回64位)
因此最上面的兩個byte是0x403F
cy86-ref是另外的神奇方式我不太懂
f80 ->u64
直接disassemble cy86-ref抄他的XD
f80 - 0x8000 0000 0000 0000 把他轉成正常int會有的範圍
=> s64
=> s64 + 0x8000 0000 0000 0000 把剛剛扣掉的東西加回去
=> u64
連jmp都不用比u64->f80好寫
600-float-calculator
如果發現前面結果都正常,多算幾個之後卻開始胡搞的話,
那應該代表FPU register stack爆了
爆了不知為何不會當掉,只是新的值塞不進去就開使用舊的亂算
主要嫌疑犯應該是FICOMP
其他運算都是pop兩個值push結果
他只pop一個然後把結果存在EFLAGS裡
沒再幫他pop一次stack就會一直往上長
不知為何沒有和FCOMPP對應的FICOMPP,那個就會自己pop乾淨了
== 其他 ==
務必想辦法讓輸出的elf可以很容易的disassemble,不然要debug幾乎不可能
static data會混淆objdump,
所以必須在elf裡放section table標記哪些是可執行區域,
和用symbol table幫助他
X86/64煩歸煩,其實做完我覺得還滿好玩的XD
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 1.34.63.98
推 Fenikso:sib真的是個很莫名其妙的東西.. 02/27 00:58
我覺得可能是用來加速array iteration的?
s是element size,i是index,b是起始位置這樣
推 baiypwup:真是好有用!剛開始折騰200,看到「大爆發」真是淚目… 02/27 01:00
→ baiypwup:為了救急先用了這個查opcode,不過還是題主的方法優雅, 02/27 01:04
上面那些寫完我也用這個檢查過,不然就糗大了XD
再補充一下machine code沒有任何硬性的alignment需求
memory access其實也沒有,效率另外再說就是了...
所以一開始可以先不用考慮這些東西,麻煩事已經夠多了
(我花了一陣子查RSP的alignment有沒有保證...結論是到時候要自己橋)
另外每個test case要debug都是半天以上,根本搞不懂為何出錯
抓到之後其實一下就解決了
還出現過以為抓到了結果是測資打錯這種慘劇
64bit hex太容易眼花了
※ 編輯: azureblaze 來自: 1.34.63.98 (02/27 01:11)
※ 編輯: azureblaze 來自: 1.34.63.98 (02/27 01:14)
→ descent:x86 machine code 真不是普通的複雜 02/27 18:29
→ descent:不知道你花了多久搞懂? 02/27 18:30
→ jackace:debug用gdb就夠了 不需要弄symbol table跟section 02/28 17:25
推 Fenikso:有個好的symbol table, objdump看code的時候會省事很多 02/28 17:48
→ jackace:pa9 test存取的變數就固定那幾個 有沒有sym tbl沒甚麼差別 02/28 23:00
→ azureblaze:後面可能還是需要 我只做了section就是了 02/28 23:47
→ baiypwup:我後來乾脆就用if..else將全部的mov的情況枚舉出來了… 03/29 03:14
→ baiypwup:一共也不是非常多行,要是按譯碼規則做也許差不多複雜吧 03/29 03:17