Re: [請益] 想不通直譯器vs編譯器vs機器碼的問題

看板Soft_Job作者 (さいでんし)時間7年前 (2018/05/16 14:21), 7年前編輯推噓4(4015)
留言19則, 6人參與, 7年前最新討論串9/9 (看更多)
Update: 睡前簡單弄個小更新 把我回文中說的code cache的hashtable快速弄出來了 第一次呼叫時會emite code,第二次因為hashtable查找成功則直接呼叫 另外就是把mmap要記憶體時的可讀可寫可執行,改成僅可讀可寫 後面再透過mprotect改成可讀可執行 這樣可以防止後來有人竄改JIT emit的code cache patch放在這邊: https://paste.plurk.com/show/2636452/ P.S. 我不用github放的原因是因為我不想暴露我的主ID 還請擔待 = = = = = = = = = = = = 中午吃飽飯有點脹氣 看到某篇回文的推文中有人想要看JIT範例 所以簡單寫了個很粗糙的版本 前後花不到15分鐘 所以覺得coding style很爛、沒效率是很正常的,別打我QQ (結果真的被轉出去了Orz 我晚點忙完會來修code 真的assembly code gen會找時間補上的) 大抵上的思路就是你吃到虛擬機的bytecode, 就把它轉成host平台的native code 接著把它塞進你跟系統動態要的、可以執行的記憶體區段 然後就很快樂的開始執行它 黃色上色的地方,每個作業系統給的API不一樣 Linux、OSX、FreeBSD ... (most of *NIX OSes): mprotect()、mmap() with proper permissions. Windows: VirtualAlloc() = = = = = = = = = = = /** * * This piece of code is to demo simple VM/JIT knowledge * and is released under 2-clause BSD license. * * Copyright 2018/05/16 snaketsai * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * U2FsdGVkX19w8Ikk1T7xBlbh4vDhIEvZzshUhXft6XMFugC9M27uV9LDszf7/8gP * OtF2AZwYaUQqzLLY5vXhCQ== * **/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #ifdef _NOJIT static inline int IAdd(int inp1, int inp2) { puts("Non-JIT version iadd."); return inp1+inp2; } static inline int ISub(int inp1, int inp2) { puts("Non-JIT version isub."); return inp1-inp2; } #else // Nope, I'm not gonna write an assembler. // Just pretend that we _magically_ get assembly pieces we need. const unsigned char _add[] = \ "\x55\x48\x89\xe5\x89\x7d\xfc\x89\x75\xf8\x8b\x55\xfc\x8b\x45\xf8\x01\xd0\x5d\xc3\x00"; const unsigned char _sub[] = \ "\x55\x48\x89\xe5\x89\x7d\xfc\x89\x75\xf8\x8b\x45\xfc\x2b\x45\xf8\x5d\xc3\x00"; #define MAXCODECAHE_SIZE 4096 void* AllocExeMem(size_t size) { void* memPtr = mmap(0, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (memPtr == (void*)-1) { perror("mmap() failed."); return NULL; } return memPtr; } void *CodeCachePool; int (*IAdd)(int, int); int (*ISub)(int, int); #endif /** Very Simple VM spec - - Every insn. is 3 byte long. opcodes are listed below: 0x00 HCF, halt and catch fire. 0x01 IADD, humbly add two signed integers and print the result. 0x02 ISUB, humbly substract two signed integers and print the result. The bytecode stream should end with a 0xff byte mark. **/ const unsigned char BytecodeStream[] = \ "\x01\x02\x03\x02\x05\x04\x00\x00\x00\xff"; int runVM(const unsigned char* bstream) { unsigned char insn[3]; while(bstream[0] != 0xff) { memcpy(&insn,bstream,3); switch(insn[0]) { case 0x00: puts("Dave, stop, will you ?"); return 0; case 0x01: #ifndef _NOJIT // emit code to code cache. memcpy(CodeCachePool, _add, sizeof(_add)); #endif printf("iadd: %d\n", IAdd((int)insn[1], (int)insn[2])); break; case 0x02: #ifndef _NOJIT memcpy(CodeCachePool, _sub, sizeof(_sub)); #endif printf("isub: %d\n", ISub((int)insn[1], (int)insn[2])); break; default : // Unrecognized insn. perror("Sorry Dave, I can't do that."); return -1; } bstream+=3; } } int main(int ac, char* av[]) { int ret = -1; #ifndef _NOJIT CodeCachePool = AllocExeMem(MAXCODECAHE_SIZE); IAdd = CodeCachePool; ISub= CodeCachePool; #endif ret = runVM(BytecodeStream); if(ret == 0) { perror("VM exited successfully."); } else { perror("Unexpected error occured."); } return 0; } -- Linux is the bone of my world. Kernel is mybody, and initramfs is myblood, have created over a thousand Distros. Unknown to impossibility. Nor known to limitation. -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 140.113.167.181 ※ 文章網址: https://www.ptt.cc/bbs/Soft_Job/M.1526451691.A.2E2.html ※ 編輯: snaketsai (140.113.167.181), 05/16/2018 14:58:35

05/16 18:51, 7年前 , 1F
只看到HAL(遮臉w
05/16 18:51, 1F

05/16 19:56, 7年前 , 2F
113就是猛 可是沒有人推文QQ
05/16 19:56, 2F
這整份code其實沒什麼技術背景,大概上過compiler課的人都會寫 唯一需要額外知道的,是如何去要一塊可塞code又可執行的記憶體 這個課堂上不會教

05/16 19:58, 7年前 , 3F
超讚
05/16 19:58, 3F

05/16 20:23, 7年前 , 4F
原來IAdd,ISub有個puts,難怪機器碼這麼長
05/16 20:23, 4F
沒欸,會那麼長是因為包含function call要的prologue跟epilogue 我其實弄得比較像intrinsic fnuction 所以Non-JIT的部份可以直接跟JIT的無縫接在一起

05/16 22:22, 7年前 , 5F
沒想到有這麼完整的write up 馬上google一下"mprotect/mmap"+"JIT"發現已經不少文章在講 我第一次知道JIT大概是2011年,當時文章還真的不多 稍微說一下我這篇目前離實用code的差距在: (1) 缺真正的asm codegen,我上面的asm snippet是用gcc -c去編C code來生的 (2) JIT emit code後沒有code reuse、每次都還會重新emit 實務上會是個hashtable,查找失敗emit、查找成功直接call 更進階一點會有所謂的block chaining,也就是把能接在一起的native code串在一起跑 這樣可以節省跳進、又跳出VM code的時間 (3) mmap權限直接開executable其實有點危險,比較正確的作法會分兩段: 先mmap開rw要記憶體、把code塞進去 然後用mprotect把它改設成rx ※ 編輯: snaketsai (140.113.167.181), 05/16/2018 23:24:51

05/17 00:05, 7年前 , 6F
想說add,sub都有instruction,怎麼會那麼長XD
05/17 00:05, 6F

05/17 00:06, 7年前 , 7F
這篇文章把可執行的部分點出來,只是沒codegen
05/17 00:06, 7F

05/17 00:07, 7年前 , 8F
也不知道可不可以叫做即時編譯
05/17 00:07, 8F

05/17 00:10, 7年前 , 9F
阿 我想到了,x86的calling convention是推入堆疊
05/17 00:10, 9F

05/17 00:10, 7年前 , 10F
也許是這樣會比較複雜吧
05/17 00:10, 10F
應該是那樣沒錯 ※ 編輯: snaketsai (140.113.167.181), 05/17/2018 02:53:13

05/17 05:09, 7年前 , 11F
05/17 05:09, 11F

05/17 17:11, 7年前 , 12F
看了一下-O0編出來的,好像真的有前後兩段
05/17 17:11, 12F

05/17 17:12, 7年前 , 13F
不過有下最佳化參數後就只剩幾行
05/17 17:12, 13F

05/17 17:13, 7年前 , 14F
可能就你說的prologue和epilogue
05/17 17:13, 14F

05/17 18:58, 7年前 , 15F
如果是add的話,gcc -O3會優化到只剩下lea就retq了XD
05/17 18:58, 15F

05/17 19:01, 7年前 , 16F
其實我現在正在看要怎麼模仿tcg的codegen來做
05/17 19:01, 16F

05/17 19:01, 7年前 , 17F
tcg在backend時只負責gen中間的部份
05/17 19:01, 17F

05/17 19:02, 7年前 , 18F
真正vcpu在cpu-exec時會macro刻意把進入換成平台相依的
05/17 19:02, 18F

05/17 19:02, 7年前 , 19F
prologue、然後才跳進去code cache、出去時叫epilogue
05/17 19:02, 19F
文章代碼(AID): #1Q-ythBY (Soft_Job)
討論串 (同標題文章)
完整討論串 (本文為第 9 之 9 篇):
文章代碼(AID): #1Q-ythBY (Soft_Job)