Guest User

Untitled

a guest
Mar 16th, 2014
467
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 33.19 KB | None | 0 0
  1. This is Google's cache of http://www.pvk.ca/Blog/2014/03/15/sbcl-the-ultimate-assembly-code-breadboard/. It is a snapshot of the page as it appeared on Mar 16, 2014 00:25:28 GMT. The current page could have changed in the meantime. Learn moreTip: To quickly find your search term on this page, press Ctrl+F or ⌘-F (Mac) and use the find bar.Text-only version
  2.  
  3. SBCL: the ultimate assembly code breadboard - Paul Khuong mostly on Lisp
  4. <meta name="description" content="Earlier this week, I took another look at the
  5. F18. As usual with Chuck Moore&#8217;s
  6. work, it&#8217;s hard to tell the difference between insanity &hellip;">
  7. MathJax.Hub.Config({
  8. TeX: {
  9. Macros: {
  10. sp: "^",
  11. sb: "_"
  12. }
  13. }});
  14. <script type="text/javascript"
  15. src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
  16. Paul Khuong mostly on Lisp
  17. RSS
  18. Blog
  19. Archives
  20. SBCL: The Ultimate Assembly Code Breadboard
  21. Mar 15th, 2014
  22. Earlier this week, I took another look at the
  23. F18. As usual with Chuck Moore&#8217;s
  24. work, it&#8217;s hard to tell the difference between insanity and mere
  25. brilliance ;) One thing that struck me is how small the stack is: 10
  26. slots, with no fancy overflow/underflow trap. The rationale is that,
  27. if you need more slots, you&#8217;re doing it wrong, and that silent
  28. overflow is useful when you know what you&#8217;re doing. That certainly
  29. jibes with my experience on the HP-41C and with x87. It also reminds
  30. me of a
  31. post of djb&#8217;s decrying our misuse of x87&#8217;s rotating stack:
  32. his thesis was that, with careful scheduling, the &#8220;free&#8221; FXCH makes
  33. the stack equivalent &#8211; if not superior &#8211; to registers. The post
  34. ends with a (non-pipelined) loop that wastes no cycle on shuffling
  35. data, thanks to the x87&#8217;s implicit stack rotation.
  36. That lead me to wonder what implementation techniques become available
  37. for stack-based VMs that restrict their stack to, e.g., 8 slots.
  38. Obviously, it would be ideal to keep everything in registers.
  39. However, if we do that naïvely, push and pop become a lot more
  40. complicated; there&#8217;s a reason why Forth engines usually cache only the
  41. top 1-2 elements of the stack.
  42. I decided to mimic the x87 and the F18: pushing/popping doesn&#8217;t cause
  43. any data movement. Instead, like the drawing below shows, they
  44. decrement/increment a modular counter that points to the top of the
  45. stack (TOS). That would still be slow in software (most ISAs can&#8217;t
  46. index registers). The key is that the counter can&#8217;t take too many
  47. values: only 8 values if there are 8 slots in the stack. Stack VMs
  48. already duplicate primops for performance reasons (e.g., to help the
  49. BTB by spreading out execution of the same primitive between multiple
  50. addresses), so it seems reasonable to specialise primitives for all 8
  51. values the stack counter can take.
  52. In a regular direct threaded VM, most primops would end with a code
  53. sequence that jumps to the next one (NEXT), something like
  54. add rsi, 8 ; increment virtual IP before jumping
  55. jmp [rsi-8] ; jump to the address RSI previously pointed to
  56. where rsi is the virtual instruction pointer, and VM instructions
  57. are simply pointers to the machine code for the relevant primitive.
  58. I&#8217;ll make two changes to this sequence. I don&#8217;t like hardcoding
  59. addresses in bytecode, and 64 bits per virtual instruction is overly
  60. wasteful. Instead, I&#8217;ll encode offsets from the primop code block:
  61. mov eax, [rsi]
  62. add rsi, 4
  63. add rax, rdi
  64. jmp rax
  65. where rdi is the base address for primops.
  66. I also need to dispatch based on the new value of the implicit stack
  67. counter. I decided to make the dispatch as easy as possible by
  68. storing the variants of each primop at regular intervals (e.g. one
  69. page). I rounded that up to 64 * 67 = 4288 bytes to minimise
  70. aliasing accidents. NEXT becomes something like
  71. mov eax, [rsi]
  72. add rsi, 4
  73. lea rax, [rax + rdi + variant_offset]
  74. jmp rax
  75. The trick is that variant_offset = 4288 * stack_counter, and the
  76. stack counter is (usually) known when the primitive is compiled. If
  77. the stack is left as is, so is the counter; pushing a value decrements
  78. the counter and popping one increments it.
  79. That seems reasonable enough. Let&#8217;s see if we can make it work.
  80. Preliminaries
  81. I want to explore a problem for which I&#8217;ll emit a lot of repetitive
  82. machine code. SLIME&#8217;s REPL and SBCL&#8217;s assembler are perfect for the
  83. task! (I hope it&#8217;s clear that I&#8217;m using unsupported internals; if it
  84. breaks, you keep the pieces.)
  85. The basic design of the VM is:
  86. r8-r15: stack slots (32 bits);
  87. rsi: base address for machine code primitives;
  88. rdi: virtual instruction pointer (points at the next instruction);
  89. rax,rbx,rcx,rdx: scratch registers;
  90. rsp: (virtual) return stack pointer .
  91. 1
  92. 2
  93. 3
  94. 4
  95. 5
  96. 6
  97. 7
  98. 8
  99. 9
  100. 10
  101. 11
  102. 12
  103. 13
  104. 14
  105. 15
  106. 16
  107. 17
  108. 18
  109. 19
  110. 20
  111. 21
  112. 22
  113. 23
  114. 24
  115. 25
  116. 26
  117. 27
  118. 28
  119. 29
  120. 30
  121. 31
  122. 32
  123. 33
  124. 34
  125. 35
  126. (import '(sb-assem:inst sb-vm::make-ea)) ; we'll use these two a lot
  127. ;; The backing store for our stack
  128. (defvar *stack* (make-array 8 :initial-contents (list sb-vm::r8d-tn
  129. sb-vm::r9d-tn
  130. sb-vm::r10d-tn
  131. sb-vm::r11d-tn
  132. sb-vm::r12d-tn
  133. sb-vm::r13d-tn
  134. sb-vm::r14d-tn
  135. sb-vm::r15d-tn)))
  136. ;; The _primop-generation-time_ stack pointer
  137. (defvar *stack-pointer*)
  138. ;; (@ 0) returns the (current) register for TOS, (@ 1) returns
  139. ;; the one just below, etc.
  140. (defun @ (i)
  141. (aref *stack* (mod (+ i *stack-pointer*) (length *stack*))))
  142. (defvar *code-base* sb-vm::rsi-tn)
  143. (defvar *virtual-ip* sb-vm::rdi-tn)
  144. (defvar *rax* sb-vm::rax-tn)
  145. (defvar *rbx* sb-vm::rax-tn)
  146. (defvar *rcx* sb-vm::rax-tn)
  147. (defvar *rdx* sb-vm::rax-tn)
  148. ;; Variants are *primitive-code-offset* bytes apart
  149. (defvar *primitive-code-offset* (* 64 67))
  150. ;; Each *stack-pointer* value gets its own code page
  151. (defstruct code-page
  152. (alloc 0) ; index of the next free byte.
  153. (code (make-array *primitive-code-offset* :element-type '(unsigned-byte 8))))
  154. The idea is that we&#8217;ll define functions to emit assembly code for each
  155. primitive; these functions will be implicitly parameterised on
  156. *stack-pointer* thanks to @. We can then call them as many times
  157. as needed to cover all values of *stack-pointer*. The only
  158. complication is that code sequences will differ in length, so we must
  159. insert padding to keep everything in sync. That&#8217;s what emit-code
  160. does:
  161. 1
  162. 2
  163. 3
  164. 4
  165. 5
  166. 6
  167. 7
  168. 8
  169. 9
  170. 10
  171. 11
  172. 12
  173. 13
  174. 14
  175. 15
  176. 16
  177. 17
  178. 18
  179. 19
  180. 20
  181. 21
  182. 22
  183. 23
  184. 24
  185. 25
  186. 26
  187. 27
  188. 28
  189. 29
  190. 30
  191. 31
  192. (defun emit-code (pages emitter)
  193. ;; there must be as many code pages as there are stack slots
  194. (assert (= (length *stack*) (length pages)))
  195. ;; find the rightmost starting point, and align to 16 bytes
  196. (let* ((alloc (logandc2 (+ 15 (reduce #'max pages :key #'code-page-alloc))
  197. 15))
  198. (bytes (loop for i below (length pages)
  199. for page = (elt pages i)
  200. collect (let ((segment (sb-assem:make-segment))
  201. (*stack-pointer* i))
  202. ;; assemble the variant for this value
  203. ;; of *stack-pointer* in a fresh code
  204. ;; segment
  205. (sb-assem:assemble (segment)
  206. ;; but first, insert padding
  207. (sb-vm::emit-long-nop segment (- alloc (code-page-alloc page)))
  208. (funcall emitter))
  209. ;; tidy up any backreference
  210. (sb-assem:finalize-segment segment)
  211. ;; then get the (position-independent) machine
  212. ;; code as a vector of bytes
  213. (sb-assem:segment-contents-as-vector segment)))))
  214. ;; finally, copy each machine code sequence to the right code page
  215. (map nil (lambda (page bytes)
  216. (let ((alloc (code-page-alloc page)))
  217. (replace (code-page-code page) bytes :start1 alloc)
  218. (assert (&lt;= (+ alloc (length bytes)) (length (code-page-code page))))
  219. (setf (code-page-alloc page) (+ alloc (length bytes)))))
  220. pages bytes)
  221. ;; and return the offset for that code sequence
  222. alloc))
  223. This function is used by emit-all-code to emit the machine code for
  224. a bunch of primitives, while tracking the start offset for each
  225. primitive.
  226. 1
  227. 2
  228. 3
  229. 4
  230. 5
  231. 6
  232. 7
  233. 8
  234. 9
  235. 10
  236. (defun emit-all-code (&rest emitters)
  237. (let ((pages (loop repeat (length *stack*)
  238. for page = (make-code-page)
  239. ;; prefill everything with one-byte NOPs
  240. do (fill (code-page-code page) #x90)
  241. collect page)))
  242. (values (mapcar (lambda (emitter)
  243. (emit-code pages emitter))
  244. emitters)
  245. pages)))
  246. Now, the pièce de résistance:
  247. 1
  248. 2
  249. 3
  250. 4
  251. 5
  252. 6
  253. 7
  254. 8
  255. 9
  256. 10
  257. 11
  258. 12
  259. 13
  260. (defun next (&optional offset)
  261. (setf offset (or offset 0)) ; accommodate primops that frob IP
  262. (let ((rotation (mod *stack-pointer* (length *stack*))))
  263. (inst movzx *rax* (make-ea :dword :index *virtual-ip*
  264. :disp offset))
  265. (unless (= -4 offset)
  266. (inst add *virtual-ip* (+ 4 offset)))
  267. (if (zerop rotation)
  268. (inst add *rax* *code-base*)
  269. (inst lea *rax* (make-ea :qword :base *code-base*
  270. :index *rax*
  271. :disp (* rotation *primitive-code-offset*))))
  272. (inst jmp *rax*)))
  273. First steps
  274. Let&#8217;s add a few simple primitives.
  275. 1
  276. 2
  277. 3
  278. 4
  279. 5
  280. 6
  281. 7
  282. 8
  283. 9
  284. 10
  285. 11
  286. 12
  287. 13
  288. 14
  289. 15
  290. 16
  291. 17
  292. 18
  293. 19
  294. 20
  295. (defun swap ()
  296. (inst xchg (@ 0) (@ 1)) ; exchange top of stack and stack[1]
  297. (next))
  298. (defun dup ()
  299. (decf *stack-pointer*) ; grow stack (which grows down)
  300. (inst mov (@ 0) (@ 1)) ; and overwrite TOS
  301. (next))
  302. (defun drop (&optional offset)
  303. (incf *stack-pointer*) ; just shrink the stack
  304. (next offset))
  305. (defun add ()
  306. (inst add (@ 1) (@ 0)) ; second element becomes TOS
  307. (drop))
  308. (defun sub ()
  309. (inst sub (@ 1) (@ 0))
  310. (drop))
  311. 1
  312. 2
  313. 3
  314. 4
  315. 5
  316. 6
  317. 7
  318. 8
  319. 9
  320. 10
  321. 11
  322. 12
  323. 13
  324. 14
  325. 15
  326. 16
  327. CL-USER> (setf *print-length* 100)
  328. 100
  329. CL-USER> (emit-all-code 'swap 'dup 'drop 'add 'sub)
  330. (0 32 64 96 128)
  331. (#S(CODE-PAGE
  332. :ALLOC 152
  333. :CODE #(69 135 193 139 4 61 0 0 0 0 72 131 199 4 72 1 240 255 224 102 15 31
  334. 132 0 0 0 0 0 15 31 64 0 69 139 248 139 4 61 0 0 0 0 72 131 199 4
  335. 72 141 132 6 64 117 0 0 255 224 15 31 132 0 0 0 0 0 139 4 61 0 0 0
  336. 0 72 131 199 4 72 141 132 6 192 16 0 0 255 224 102 15 31 132 0 0 0
  337. 0 0 102 144 69 1 193 139 ...))
  338. ...)
  339. CL-USER> (defparameter *code0* (code-page-code (first (second /))))
  340. *CODE*
  341. CL-USER> (defparameter *code1* (code-page-code (second (second //))))
  342. *CODE*
  343. The code for swap lives between bytes 0 and 32. Let&#8217;s take a look
  344. at the version for *stack-pointer* = 0 and *stack-pointer* = 1.
  345. 1
  346. 2
  347. 3
  348. 4
  349. 5
  350. 6
  351. 7
  352. 8
  353. 9
  354. 10
  355. 11
  356. 12
  357. 13
  358. 14
  359. 15
  360. 16
  361. 17
  362. 18
  363. 19
  364. 20
  365. 21
  366. 22
  367. 23
  368. CL-USER> (sb-sys:with-pinned-objects (*code0*)
  369. (sb-disassem:disassemble-memory (sb-sys:vector-sap *code0*)
  370. 32))
  371. ; Size: 32 bytes
  372. ; 0669C700: 4587C1 XCHG R8D, R9D
  373. ; 03: 8B043D00000000 MOV EAX, [RDI]
  374. ; 0A: 4883C704 ADD RDI, 4
  375. ; 0E: 4801F0 ADD RAX, RSI
  376. ; 11: FFE0 JMP RAX
  377. ; 13: 660F1F840000000000 NOP ; padding NOPs
  378. ; 1C: 0F1F4000 NOP
  379. NIL
  380. CL-USER> (sb-sys:with-pinned-objects (*code1*)
  381. (sb-disassem:disassemble-memory (sb-sys:vector-sap *code1*)
  382. 32))
  383. ; Size: 32 bytes
  384. ; 0669D810: 4587CA XCHG R9D, R10D
  385. ; 13: 8B043D00000000 MOV EAX, [RDI]
  386. ; 1A: 4883C704 ADD RDI, 4
  387. ; 1E: 488D8406C0100000 LEA RAX, [RSI+RAX+4288]
  388. ; 26: FFE0 JMP RAX
  389. ; 28: 0F1F840000000000 NOP
  390. NIL
  391. dup is at 32-64, and sub at 128-152:
  392. 1
  393. 2
  394. 3
  395. 4
  396. 5
  397. 6
  398. 7
  399. 8
  400. 9
  401. 10
  402. 11
  403. 12
  404. 13
  405. 14
  406. 15
  407. 16
  408. 17
  409. 18
  410. 19
  411. 20
  412. 21
  413. CL-USER> (sb-sys:with-pinned-objects (*code0*)
  414. (sb-disassem:disassemble-memory (sb-sys:sap+ (sb-sys:vector-sap *code0*) 32)
  415. 32))
  416. ; Size: 32 bytes
  417. ; 0669C720: 458BF8 MOV R15D, R8D
  418. ; 23: 8B043D00000000 MOV EAX, [RDI]
  419. ; 2A: 4883C704 ADD RDI, 4
  420. ; 2E: 488D840640750000 LEA RAX, [RSI+RAX+30016]
  421. ; 36: FFE0 JMP RAX
  422. ; 38: 0F1F840000000000 NOP
  423. NIL
  424. CL-USER> (sb-sys:with-pinned-objects (*code0*)
  425. (sb-disassem:disassemble-memory (sb-sys:sap+ (sb-sys:vector-sap *code0*) 128)
  426. 24))
  427. ; Size: 24 bytes
  428. ; 0669C780: 4529C1 SUB R9D, R8D
  429. ; 83: 8B043D00000000 MOV EAX, [RDI]
  430. ; 8A: 4883C704 ADD RDI, 4
  431. ; 8E: 488D8406C0100000 LEA RAX, [RSI+RAX+4288]
  432. ; 96: FFE0 JMP RAX
  433. NIL
  434. These are relatively tight. I certainly like how little data
  435. shuffling there is; the NEXT sequence is a bit hairy, but the
  436. indirect branch is likely its weakest (and least avoidable) point.
  437. Control flow primops
  438. A VM without control flow isn&#8217;t even a toy. First, unconditional
  439. relative jumps. These can be encoded as [jmp] [offset], with the 32
  440. bit offset relative to the end of offset. We just overwrite
  441. *virtual-ip* with the new address.
  442. 1
  443. 2
  444. 3
  445. 4
  446. 5
  447. (defun jmp ()
  448. (inst movsx *rax* (make-ea :dword :base *virtual-ip*))
  449. (inst lea *virtual-ip* (make-ea :dword :base *virtual-ip* :index *rax*
  450. :disp 4))
  451. (next))
  452. Call and return are at the heart of Forth-like engines. ret is
  453. easy: just pop from the control stack into *virtual-ip*.
  454. 1
  455. 2
  456. 3
  457. (defun ret ()
  458. (inst pop *virtual-ip*)
  459. (next))
  460. Call is a bit more complicated. It&#8217;s like jmp, but pushes the
  461. address of the next instruction to the control stack:
  462. 1
  463. 2
  464. 3
  465. 4
  466. 5
  467. 6
  468. (defun call ()
  469. (inst movsx *rax* (make-ea :dword :base *virtual-ip*))
  470. (inst add *virtual-ip* 4)
  471. (inst push *virtual-ip*)
  472. (inst add *virtual-ip* *rax*)
  473. (next))
  474. Let&#8217;s look at the resulting machine code.
  475. 1
  476. 2
  477. 3
  478. 4
  479. 5
  480. 6
  481. 7
  482. 8
  483. 9
  484. 10
  485. 11
  486. 12
  487. 13
  488. 14
  489. 15
  490. 16
  491. 17
  492. 18
  493. 19
  494. 20
  495. 21
  496. 22
  497. 23
  498. 24
  499. 25
  500. 26
  501. 27
  502. 28
  503. 29
  504. 30
  505. 31
  506. 32
  507. 33
  508. 34
  509. 35
  510. 36
  511. CL-USER> (emit-all-code 'jmp 'ret 'call)
  512. (0 32 64)
  513. (#S(CODE-PAGE
  514. :ALLOC 91
  515. :CODE #(72 99 7 72 141 124 7 4 139 4 61 0 0 0 0 72 131 199 4 72 1 240 255
  516. 224 15 31 132 0 0 0 0 0 95 139 4 61 0 0 0 0 72 131 199 4 72 1 240
  517. 255 224 102 15 31 132 0 0 0 0 0 102 15 31 68 0 0 72 99 7 72 131 199
  518. 4 87 72 1 199 139 4 61 0 0 0 0 72 131 199 4 72 1 240 255 224 144
  519. 144 144 144 144 144 144 144 144 ...))
  520. ...)
  521. CL-USER> (let ((code (code-page-code (first (second /)))))
  522. (sb-sys:with-pinned-objects (code)
  523. (sb-disassem:disassemble-memory (sb-sys:vector-sap code) 91)))
  524. ; Size: 91 bytes
  525. ; 08395200: 486307 MOVSXD RAX, DWORD PTR [RDI] ; jmp
  526. ; 03: 488D7C0704 LEA RDI, [RDI+RAX+4]
  527. ; 08: 8B043D00000000 MOV EAX, [RDI]
  528. ; 0F: 4883C704 ADD RDI, 4
  529. ; 13: 4801F0 ADD RAX, RSI
  530. ; 16: FFE0 JMP RAX
  531. ; 18: 0F1F840000000000 NOP
  532. ; 20: 5F POP RDI ; ret
  533. ; 21: 8B043D00000000 MOV EAX, [RDI]
  534. ; 28: 4883C704 ADD RDI, 4
  535. ; 2C: 4801F0 ADD RAX, RSI
  536. ; 2F: FFE0 JMP RAX
  537. ; 31: 660F1F840000000000 NOP
  538. ; 3A: 660F1F440000 NOP
  539. ; 40: 486307 MOVSXD RAX, DWORD PTR [RDI] ; call
  540. ; 43: 4883C704 ADD RDI, 4
  541. ; 47: 57 PUSH RDI
  542. ; 48: 4801C7 ADD RDI, RAX
  543. ; 4B: 8B043D00000000 MOV EAX, [RDI]
  544. ; 52: 4883C704 ADD RDI, 4
  545. ; 56: 4801F0 ADD RAX, RSI
  546. ; 59: FFE0 JMP RAX
  547. FFI from SBCL
  548. We finally almost have enough for interesting demos. The only
  549. important thing that&#8217;s still missing is calls from CL into the VM.
  550. I&#8217;ll assume that the caller takes care of saving any important
  551. register, and that the primop (rsi) and virtual IP (rdi) registers
  552. are setup correctly. The stack will be filled on entry, by copying
  553. values from the buffer rax points to, and written back on exit.
  554. 1
  555. 2
  556. 3
  557. 4
  558. 5
  559. 6
  560. 7
  561. 8
  562. 9
  563. 10
  564. 11
  565. 12
  566. 13
  567. 14
  568. 15
  569. 16
  570. 17
  571. 18
  572. 19
  573. (defun enter ()
  574. (inst push sb-vm::rbp-tn)
  575. (inst mov sb-vm::rbp-tn sb-vm::rsp-tn) ; setup a normal frame
  576. (inst push *rax*) ; stash rax
  577. (dotimes (i 8) ; copy the stack in
  578. (inst mov (@ i) (make-ea :dword :base *rax* :disp (* 4 i))))
  579. (next))
  580. (defun leave ()
  581. ;; restore RAX
  582. (inst mov *rax* (make-ea :qword :base sb-vm::rbp-tn :disp -8))
  583. ;; overwrite the output stack
  584. (dotimes (i 8)
  585. (inst mov (make-ea :dword :base *rax* :disp (* 4 i))
  586. (@ i)))
  587. ;; and unwind the frame
  588. (inst mov sb-vm::rsp-tn sb-vm::rbp-tn)
  589. (inst pop sb-vm::rbp-tn)
  590. (inst ret))
  591. The CL-side interlocutor of enter follows, as a VOP:
  592. 1
  593. 2
  594. 3
  595. 4
  596. 5
  597. 6
  598. 7
  599. 8
  600. 9
  601. 10
  602. 11
  603. 12
  604. 13
  605. 14
  606. 15
  607. 16
  608. 17
  609. 18
  610. 19
  611. 20
  612. 21
  613. 22
  614. 23
  615. 24
  616. 25
  617. 26
  618. 27
  619. 28
  620. 29
  621. 30
  622. 31
  623. 32
  624. 33
  625. 34
  626. 35
  627. 36
  628. 37
  629. 38
  630. 39
  631. 40
  632. 41
  633. 42
  634. 43
  635. 44
  636. 45
  637. 46
  638. 47
  639. 48
  640. 49
  641. 50
  642. 51
  643. 52
  644. 53
  645. 54
  646. 55
  647. 56
  648. 57
  649. 58
  650. 59
  651. 60
  652. 61
  653. 62
  654. 63
  655. (sb-c:defknown %enter-vm (system-area-pointer system-area-pointer system-area-pointer) (values)
  656. (sb-c:any) :overwrite-fndb-silently t)
  657. (in-package "SB-VM")
  658. (sb-vm::define-vop (cl-user::%enter-vm)
  659. (:translate cl-user::%enter-vm)
  660. (:policy :fast-safe)
  661. (:args (stack :scs (sap-reg) :target rax)
  662. (code :scs (sap-reg) :target rdi)
  663. (primitives :scs (sap-reg) :target rsi))
  664. (:arg-types system-area-pointer system-area-pointer system-area-pointer)
  665. (:results)
  666. (:temporary (:sc sap-reg :offset rax-offset :from (:argument 0)) rax)
  667. (:temporary (:sc sap-reg :offset rbx-offset :from :eval) rbx)
  668. (:temporary (:sc sap-reg :offset rcx-offset :from :eval) rcx)
  669. (:temporary (:sc sap-reg :offset rdx-offset :from :eval) rdx)
  670. (:temporary (:sc sap-reg :offset rdi-offset :from (:argument 1)) rdi)
  671. (:temporary (:sc sap-reg :offset rsi-offset :from (:argument 2)) rsi)
  672. (:ignore rbx rcx rdx)
  673. (:generator 0
  674. (inst push r8-tn) ;; the stack was just too painful to declare
  675. (inst push r9-tn) ;; as temporary... and it overwrites the
  676. (inst push r10-tn) ;; thread-base TN, which regalloc disregards.
  677. (inst push r11-tn)
  678. (inst push r12-tn)
  679. (inst push r13-tn)
  680. (inst push r14-tn)
  681. (inst push r15-tn)
  682. (move rax stack)
  683. (move rdi code)
  684. (move rsi primitives)
  685. (inst call rsi) ;; assume the code page starts with ENTER
  686. (inst pop r15-tn)
  687. (inst pop r14-tn)
  688. (inst pop r13-tn)
  689. (inst pop r12-tn)
  690. (inst pop r11-tn)
  691. (inst pop r10-tn)
  692. (inst pop r9-tn)
  693. (inst pop r8-tn)))
  694. (in-package "CL-USER")
  695. (defun %enter-vm (stack-sap bytecode-sap primitives-sap)
  696. (declare (type system-area-pointer stack-sap bytecode-sap primitives-sap))
  697. (%enter-vm stack-sap bytecode-sap primitives-sap))
  698. (defun vm (stack-designator bytecode primitives)
  699. (declare (type (simple-array (unsigned-byte 32) 1) bytecode)
  700. (type system-area-pointer primitives))
  701. ;; initialise the stack with the designator.
  702. (let ((stack (make-array 8 :element-type '(unsigned-byte 32)
  703. :initial-element 0)))
  704. (if (typep stack-designator 'sequence)
  705. (map-into stack (lambda (x)
  706. (ldb (byte 32 0) x))
  707. stack-designator)
  708. (fill stack (ldb (byte 32 0) stack-designator)))
  709. (sb-sys:with-pinned-objects (stack bytecode)
  710. (time (%enter-vm (sb-sys:vector-sap stack) (sb-sys:vector-sap bytecode)
  711. primitives)))
  712. stack))
  713. The only thing missing is to store the machine code for our primop in
  714. a range of memory that&#8217;s executable.
  715. 1
  716. 2
  717. 3
  718. 4
  719. 5
  720. 6
  721. 7
  722. 8
  723. 9
  724. 10
  725. 11
  726. 12
  727. 13
  728. 14
  729. 15
  730. 16
  731. 17
  732. 18
  733. 19
  734. 20
  735. 21
  736. 22
  737. 23
  738. 24
  739. 25
  740. 26
  741. ;; first, get an executable range of memory
  742. (defvar *code-page* (sb-posix:mmap nil (* (length *stack*) *primitive-code-offset*)
  743. (logior sb-posix:prot-read sb-posix:prot-write sb-posix:prot-exec)
  744. (logior sb-posix:map-anon sb-posix:map-private)
  745. -1 0))
  746. ;; our list of primop names
  747. (defvar *primops* '(enter leave
  748. swap dup drop
  749. add sub
  750. jmp
  751. call ret))
  752. (defun assemble-primops ()
  753. (multiple-value-bind (offsets pages)
  754. (apply 'emit-all-code *primops*)
  755. (loop for page in pages
  756. for offset upfrom 0 by *primitive-code-offset*
  757. do (sb-kernel:copy-ub8-to-system-area
  758. (code-page-code page) 0
  759. *code-page* offset
  760. *primitive-code-offset*))
  761. (mapcar 'cons *primops* offsets)))
  762. ;; this alist maps primop names to offsets
  763. (defparameter *primops-offsets* (assemble-primops))
  764. 1
  765. 2
  766. 3
  767. CL-USER> *primops-offsets*
  768. ((ENTER . 0) (LEAVE . 64) (SWAP . 112) (DUP . 144) (DROP . 176) (ADD . 208)
  769. (SUB . 240) (JMP . 272) (CALL . 304) (RET . 336))
  770. Let&#8217;s execute add sub (and finish with leave).
  771. 1
  772. 2
  773. 3
  774. 4
  775. 5
  776. 6
  777. 7
  778. 8
  779. 9
  780. CL-USER> (vm '(3 2 10) (coerce '(208 240 64) '(simple-array (unsigned-byte 32) 1)) *code-page*)
  781. Evaluation took:
  782. 0.000 seconds of real time
  783. 0.000003 seconds of total run time (0.000002 user, 0.000001 system)
  784. 100.00% CPU
  785. 2,288 processor cycles
  786. 0 bytes consed
  787. #(5 0 0 0 0 0 3 5)
  788. And, indeed, 10 - (3 + 2) = 5.
  789. We should also test function calls
  790. 1
  791. 2
  792. 3
  793. 4
  794. 5
  795. 6
  796. 7
  797. 8
  798. 9
  799. 10
  800. 11
  801. 12
  802. 13
  803. 14
  804. 15
  805. 16
  806. CL-USER> (vm '(3 2 10) (coerce '(304 8 ; call F
  807. 240
  808. 64
  809. ;; F:
  810. 208
  811. 336)
  812. '(simple-array (unsigned-byte 32) 1))
  813. *code-page*)
  814. Evaluation took:
  815. 0.000 seconds of real time
  816. 0.000005 seconds of total run time (0.000003 user, 0.000002 system)
  817. 100.00% CPU
  818. 2,640 processor cycles
  819. 0 bytes consed
  820. #(5 0 0 0 0 0 3 5)
  821. Instead of executing add directly, this bytecode sequence calls to
  822. whatever is 8 bytes (2 dwords) after the call instruction; in our
  823. case, add ret.
  824. Writing bytecode by hand is annoying. This tiny functions takes care
  825. of the stupid stuff.
  826. 1
  827. 2
  828. 3
  829. 4
  830. 5
  831. 6
  832. 7
  833. (defun assemble (opcodes)
  834. (map '(simple-array (unsigned-byte 32) 1)
  835. (lambda (opcode)
  836. (if (integerp opcode)
  837. (ldb (byte 32 0) opcode)
  838. (cdr (assoc opcode *primops-offsets*))))
  839. opcodes))
  840. We can now write
  841. 1
  842. 2
  843. 3
  844. 4
  845. 5
  846. 6
  847. 7
  848. CL-USER> (vm '(3 2 10) (assemble '(call 8
  849. sub
  850. leave
  851. add
  852. ret))
  853. *code-page*)
  854. Conditionals
  855. We can now either write (basically) straightline code or infinite
  856. loops. We need conditionals. Their implementation is much like
  857. jmp, with a tiny twist. Let&#8217;s start with jump if (top of stack is)
  858. non-zero and jump if zero.
  859. 1
  860. 2
  861. 3
  862. 4
  863. 5
  864. 6
  865. 7
  866. 8
  867. 9
  868. 10
  869. 11
  870. 12
  871. 13
  872. 14
  873. 15
  874. 16
  875. 17
  876. (defun jcc (cc)
  877. ;; next word is the offset if the condition is met, otherwise,
  878. ;; fall through.
  879. (inst movsx *rax* (make-ea :dword :base *virtual-ip*))
  880. (inst lea *rax* (make-ea :dword :base *virtual-ip* :index *rax*
  881. :disp 4))
  882. (inst add *virtual-ip* 4)
  883. (inst test (@ 0) (@ 0))
  884. ;; update *virtual-ip* only if zero/non-zero
  885. (inst cmov cc *virtual-ip* *rax*)
  886. (next))
  887. (defun jnz ()
  888. (jcc :nz))
  889. (defun jz ()
  890. (jcc :z))
  891. Immediates
  892. It&#8217;s hard to write programs without immediate values. Earlier control
  893. flow primitives already encode immediate data in the virtual
  894. instruction stream. We&#8217;ll do the same for lit, inc, and dec:
  895. 1
  896. 2
  897. 3
  898. 4
  899. 5
  900. 6
  901. 7
  902. 8
  903. 9
  904. 10
  905. 11
  906. 12
  907. (defun lit ()
  908. (decf *stack-pointer*) ; grow the stack
  909. (inst mov (@ 0) (make-ea :dword :base *virtual-ip*)) ; load the next word
  910. (next 4)) ; and skip to the next instruction
  911. (defun inc ()
  912. (inst add (@ 0) (make-ea :dword :base *virtual-ip*))
  913. (next 4))
  914. (defun dec ()
  915. (inst sub (@ 0) (make-ea :dword :base *virtual-ip*))
  916. (next 4))
  917. My first loop
  918. Finally, we have enough for a decent-looking (if useless) loop. First,
  919. update the primop code page:
  920. 1
  921. 2
  922. 3
  923. 4
  924. 5
  925. 6
  926. 7
  927. 8
  928. ;; C-M-x to force re-evaluation, or defparameter
  929. (defvar *primops* '(enter leave lit
  930. swap dup drop
  931. add sub inc dec
  932. jmp jnz jz
  933. call ret))
  934. (defvar *primops-offsets* (assemble-primops))
  935. 1
  936. 2
  937. 3
  938. 4
  939. 5
  940. 6
  941. 7
  942. 8
  943. 9
  944. 10
  945. CL-USER> (vm '(1000000) (assemble '(lit 1 sub jnz -20 leave))
  946. *code-page*)
  947. Evaluation took:
  948. 0.009 seconds of real time
  949. 0.009464 seconds of total run time (0.009384 user, 0.000080 system)
  950. 100.00% CPU
  951. 14,944,792 processor cycles
  952. 0 bytes consed
  953. #(0 0 0 0 0 0 0 1)
  954. One million iterations of this stupid loop that only decrements a
  955. counter took 15M cycles. 15 cycles/iteration really isn&#8217;t that
  956. bad… especially considering that it executes an indirect jump after
  957. loading 1, after subtracting, and after comparing with 0.
  958. We can do better by fusing lit sub into dec:
  959. 1
  960. 2
  961. 3
  962. 4
  963. 5
  964. 6
  965. 7
  966. 8
  967. 9
  968. 10
  969. CL-USER> (vm '(1000000) (assemble '(dec 1 jnz -16 leave))
  970. *code-page*)
  971. Evaluation took:
  972. 0.007 seconds of real time
  973. 0.006905 seconds of total run time (0.006848 user, 0.000057 system)
  974. 100.00% CPU
  975. 11,111,128 processor cycles
  976. 0 bytes consed
  977. #(0 0 0 0 0 0 0 0)
  978. Fuse all the things!
  979. Decrementing a counter and jumping if non zero is a common operation
  980. (old x86 even implemented that in hardware, with loop). Let&#8217;s add
  981. decrement and jump if non-zero (djn) to the VM:
  982. 1
  983. 2
  984. 3
  985. 4
  986. 5
  987. 6
  988. 7
  989. 8
  990. 9
  991. 10
  992. 11
  993. 12
  994. 13
  995. 14
  996. 15
  997. 16
  998. (defun djn ()
  999. (inst movsx *rax* (make-ea :dword :base *virtual-ip*))
  1000. (inst lea *rax* (make-ea :dword :base *virtual-ip* :index *rax*
  1001. :disp 4))
  1002. (inst add *virtual-ip* 4)
  1003. (inst sub (@ 0) 1)
  1004. (inst cmov :nz *virtual-ip* *rax*)
  1005. (next))
  1006. (defvar *primops* '(enter leave lit
  1007. swap dup drop
  1008. add sub inc dec
  1009. jmp jnz jz djn
  1010. call ret))
  1011. (defvar *primops-offsets* (assemble-primops))
  1012. 1
  1013. 2
  1014. 3
  1015. 4
  1016. 5
  1017. 6
  1018. 7
  1019. 8
  1020. 9
  1021. 10
  1022. CL-USER> (vm '(1000000) (assemble '(djn -8 leave))
  1023. *code-page*)
  1024. Evaluation took:
  1025. 0.005 seconds of real time
  1026. 0.005575 seconds of total run time (0.005542 user, 0.000033 system)
  1027. 120.00% CPU
  1028. 8,823,896 processor cycles
  1029. 0 bytes consed
  1030. #(0 0 0 0 0 0 0 0)
  1031. That&#8217;s better… But I&#8217;m really not convinced by the conditional move.
  1032. The branch will usually be predictable, so it makes sense to expose
  1033. that to the hardware and duplicate the NEXT sequence.
  1034. 1
  1035. 2
  1036. 3
  1037. 4
  1038. 5
  1039. 6
  1040. 7
  1041. 8
  1042. 9
  1043. 10
  1044. (defun djn2 ()
  1045. (sb-assem:assemble ()
  1046. (inst sub (@ 0) 1)
  1047. (inst jmp :z fallthrough)
  1048. (inst movsx *rax* (make-ea :dword :base *virtual-ip*))
  1049. (inst lea *virtual-ip* (make-ea :dword :base *virtual-ip* :index *rax*
  1050. :disp 8))
  1051. (next -4) ; might as well pre-increment *virtual-ip*
  1052. fallthrough ; ASSEMBLE parses for labels, like TAGBODY
  1053. (next 4)))
  1054. The resulting code isn&#8217;t too large, and the two indirect jumps are 16
  1055. bytes apart.
  1056. 1
  1057. 2
  1058. 3
  1059. 4
  1060. 5
  1061. 6
  1062. 7
  1063. 8
  1064. 9
  1065. 10
  1066. 11
  1067. 12
  1068. 13
  1069. 14
  1070. 15
  1071. ; Size: 64 bytes
  1072. ; 00510220: 4183E801 SUB R8D, 1
  1073. ; 24: 7414 JEQ L0
  1074. ; 26: 486307 MOVSXD RAX, DWORD PTR [RDI]
  1075. ; 29: 488D7C0708 LEA RDI, [RDI+RAX+8]
  1076. ; 2E: 8B043DFCFFFFFF MOV EAX, [RDI-4]
  1077. ; 35: 4801F0 ADD RAX, RSI
  1078. ; 38: FFE0 JMP RAX
  1079. ; 3A: L0: 8B043D04000000 MOV EAX, [RDI+4]
  1080. ; 41: 4883C708 ADD RDI, 8
  1081. ; 45: 4801F0 ADD RAX, RSI
  1082. ; 48: FFE0 JMP RAX
  1083. ; 4A: 660F1F840000000000 NOP
  1084. ; 53: 660F1F840000000000 NOP
  1085. ; 5C: 0F1F4000 NOP
  1086. This alternative implementation does work better on our stupid loop.
  1087. 1
  1088. 2
  1089. 3
  1090. 4
  1091. 5
  1092. 6
  1093. 7
  1094. 8
  1095. 9
  1096. 10
  1097. CL-USER> (vm '(1000000) (assemble '(djn2 -8 leave))
  1098. *code-page*)
  1099. Evaluation took:
  1100. 0.004 seconds of real time
  1101. 0.004034 seconds of total run time (0.003913 user, 0.000121 system)
  1102. 100.00% CPU
  1103. 6,183,488 processor cycles
  1104. 0 bytes consed
  1105. #(0 0 0 0 0 0 0 0)
  1106. Let&#8217;s see how that compares to straight assembly code.
  1107. 1
  1108. 2
  1109. 3
  1110. 4
  1111. 5
  1112. 6
  1113. (defun ubench ()
  1114. (sb-assem:assemble ()
  1115. head
  1116. (inst sub (@ 0) 1)
  1117. (inst jmp :nz head)
  1118. (next)))
  1119. 1
  1120. 2
  1121. 3
  1122. 4
  1123. 5
  1124. 6
  1125. 7
  1126. 8
  1127. 9
  1128. 10
  1129. CL-USER> (vm '(1000000) (assemble '(ubench leave))
  1130. *code-page*)
  1131. Evaluation took:
  1132. 0.000 seconds of real time
  1133. 0.000629 seconds of total run time (0.000628 user, 0.000001 system)
  1134. 100.00% CPU
  1135. 1,001,904 processor cycles
  1136. 0 bytes consed
  1137. #(0 0 0 0 0 0 0 0)
  1138. My slow macbook air gets 1 iteration/cycle on a loop that&#8217;s 100%
  1139. control overhead. With djn2, a good implementation of a reasonable
  1140. specialised operator, the loop is about 6x as slow as native code. A
  1141. worse implementation of djn is still only 8x as slow as pure native
  1142. code, and horribly unspecialised bytecode is 11-15x as slow as native
  1143. code.
  1144. Verdict
  1145. Specialising primops to a virtual stack pointer is feasible in
  1146. practice, when the stack is restricted to a small size. It also seems
  1147. to have a reasonable runtime overhead for threaded interpreters. I&#8217;m
  1148. not actually interested in straight stack languages; however, I
  1149. believe that a fixed stack VM makes a nice runtime IR makes a nice
  1150. runtime IR, when coupled with a mechanism for local variables. We&#8217;ll
  1151. see if I find time to translate a high level language into
  1152. superoperators for such a VM. Fused operators would reduce the
  1153. importance of NEXT; in constrast, simpler function calls (because
  1154. there&#8217;s less shuffling of items to stack them up in the right
  1155. position) would remain as useful.
  1156. SBCL has definitely proven itself to be a good companion to explore
  1157. the generation of domain-specific machine code. I don&#8217;t know of any
  1158. other language implementation with that kind of support for
  1159. interactive programming and machine code generation (and inspection).
  1160. FWIW, I believe LuaJIT + dynasm will soon be comparable.
  1161. Steel Bank Common Lisp: because sometimes C abstracts away too much ;)
  1162. Posted by Paul Khuong
  1163. Mar 15th, 2014
  1164. Tweet
  1165. &laquo; Reference books I'm bringing across the border
  1166. Comments
  1167. Please enable JavaScript to view the comments powered by Disqus.
  1168. Recent Posts
  1169. SBCL: the ultimate assembly code breadboard
  1170. Reference books I'm bringing across the border
  1171. How bad can 1GB pages be?
  1172. A whirlwind introduction to decomposition methods in discrete optimisation
  1173. So you want to write an LP solver (a.k.a. My Little LP: Cholesky Is Magic)
  1174. The other Robin Hood hashing
  1175. The weaknesses of SBCL's type propagation
  1176. All you need is call/cc
  1177. Bitsets match regular expressions, compactly
  1178. Fresh in SBCL 1.1.8: SSE intrinsics!
  1179. Copyright &copy; 2014 -
  1180. Paul Khuong -
  1181. Powered by Octopress
  1182. var disqus_shortname = 'pvk';
  1183. // var disqus_developer = 1;
  1184. var disqus_identifier = 'http://www.pvk.ca/Blog/2014/03/15/sbcl-the-ultimate-assembly-code-breadboard/';
  1185. var disqus_url = 'http://www.pvk.ca/Blog/2014/03/15/sbcl-the-ultimate-assembly-code-breadboard/';
  1186. var disqus_script = 'embed.js';
  1187. (function () {
  1188. var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
  1189. dsq.src = 'http://' + disqus_shortname + '.disqus.com/' + disqus_script;
  1190. (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
  1191. }());
  1192. (function(){
  1193. var twitterWidgets = document.createElement('script');
  1194. twitterWidgets.type = 'text/javascript';
  1195. twitterWidgets.async = true;
  1196. twitterWidgets.src = 'http://platform.twitter.com/widgets.js';
  1197. document.getElementsByTagName('head')[0].appendChild(twitterWidgets);
  1198. })();
  1199. (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  1200. (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  1201. m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  1202. })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
  1203. ga('create', 'UA-20468541-1', 'pvk.ca');
  1204. ga('send', 'pageview');
Add Comment
Please, Sign In to add comment