summaryrefslogtreecommitdiff
path: root/miniany/doc/blog.jeff.over.bz_assembly_compilers_jit_2017_01_15_x86-assembler.txt
blob: f712d28bea389e2be42b39e2206b73e142f78db6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
   #[1]256LOL RSS Feed

[2][headshot-circle.png]

   256 LINES OR LESS
   (jeff overbey's blog)
     * [3]22 Apr 2018 Building a Go Doctor Refactoring
     * [4]09 Sep 2017 Lexical Analysis
     * [5]01 Jun 2017 On Performance Improvements
     * [6]30 Mar 2017 Executing Dynamically Generated Machine Code: The
       Start of a JIT
     * [7]15 Feb 2017 Finding Machine Language Encodings
     * [8]15 Jan 2017 An x86 Assembler in 256 LOC
     * [9]15 Dec 2016 256 Lines or Less (the joverblog?)

   [10]RSS Feed o [11]My Web Site
   [sidebar-button.png]
   [empty.jpg]

An x86 Assembler in 256 LOC

   15 Jan 2017

   For the first "real" post in this blog, we'll build an x86 assembler in
   less than 256 lines of C code. Obviously, it won't implement every x86
   instruction, but it will implement a surprisingly useful subset: data
   movement, control flow, integer arithmetic, bitwise operations, and
   function calls. We won't be able to run the generated machine code yet
   (that's coming in a later blog post), but we'll be in a good position
   to do so.

   I'll assume you're already familiar with x86 assembly language
   (hopefully the table below will serve as a brief refresher), although I
   won't assume you know about their machine language encodings. I'll also
   assume that you're familiar with hexadecimal representation and
   arithmetic (e.g., 9 + 1 = A and 10 - 1 = F).

1. Which instructions will we support?

   By the time we finish, we'll have an assembler that supports all of the
   following x86 instructions (yes, I'm serious):
   Instruction    Example    Description of the Example
   nop   nop   No operation (do nothing)
           -- Data Movement --
   mov register, immediate   mov eax, 0F00Dh   Place the value F00D
   (hexadecimal) in EAX
   mov register, register   mov eax, ebx   Copy the value from the EBX
   register into EAX
   mov register, [register]   mov eax, [ebx]   Treat EBX as pointer; load
   32-bit value from memory into EAX
   mov [register], register   mov [eax], ebx   Treat EAX as pointer; store
   32-bit value from EBX in memory
           -- Arithmetic --
   add register, register   add eax, ebx   EAX = EAX + EBX
   cdq   cdq   Sign-extend EAX into EDX in preparation for idiv
   dec register   dec eax   EAX = EAX - 1
   div register   div ebx   Unsigned division: EDX:EAX ÷ EBX,
   setting EAX = quotient, EDX = remainder
   idiv register   idiv ebx   Signed division: EDX:EAX ÷ EBX,
   setting EAX = quotient, EDX = remainder
   imul register   imul ebx   Signed multiplication: EDX:EAX = EAX × EBX
   inc register   inc eax   EAX = EAX + 1
   neg register   neg eax   EAX = -EAX
   mul register   mul ebx   Unsigned multiplication: EDX:EAX = EAX × EBX
   sub register, register   sub eax, ebx   EAX = EAX - EBX
           -- Bitwise Operations --
   and register, register   and eax, ebx   EAX = EAX & EBX
   not register   not eax   EAX = ~EAX
   or register, register   or eax, ebx   EAX = EAX | EBX
   sar register, immediate   sar eax, 2   Shift EAX right by 2 bits
   (sign-fill)
   sar register, cl   sar eax, cl   Shift EAX right by CL bits (sign-fill)
   shl register, immediate   shl eax, 2   Shift EAX left by 2 bits
   shl register, cl   shl eax, cl   Shift EAX left by number of bits in CL
   shr register, immediate   shr eax, 2   Shift EAX right by 2 bits
   (zero-fill)
   shr register, cl   shr eax, cl   Shift EAX right by CL bits (zero-fill)
   xor register, register   xor eax, ebx   EAX = EAX ^ EBX
           -- Comparison --
   cmp register, register   cmp eax, ebx   Compare EAX to EBX, setting
   flags for conditional jump
           -- Control Flow --
   jmp bytes   jmp -10   Jump -10 bytes, i.e., move EIP backward by 10
   bytes
   ja bytes   ja -10   Jump if above (>, unsigned)
   jae bytes   jae -10   Jump if above or equal (>=, unsigned)
   jb bytes   jb -10   Jump if below (<, unsigned)
   jbe bytes   jbe -10   Jump if below or equal (<=, unsigned)
   je bytes   je -10   Jump if equal
   jg bytes   jg -10   Jump if greater (>, signed)
   jge bytes   jge -10   Jump if greater or equal (>=, signed)
   jl bytes   jl -10   Jump if less (<, signed)
   jle bytes   jle -10   Jump if less or equal (<=, signed)
   jne bytes   jne -10   Jump if not equal
           -- Function Calls --
   call register   call eax   Call function at pointer stored in EAX
   push register   push eax   Push value of EAX onto the stack
   pop register   pop eax   Pop a value from the stack into EAX
   ret immediate   ret 4   Return from function, removing 4 bytes of stack
   arguments

2. The API: x86asm.h

   The header file, x86asm.h, defines the API that we intend for clients
   to use. It provides
     * an enumeration of the x86's 32-bit registers (reg32_t), and
     * one function for each instruction form we can assemble.

   Here's the header in its entirety. (There's more explanation in the
   next section, but it will be helpful to read through the header file
   first.)
// x86 Subset Assembler - API
//-----------------------------------------------------------------------------
// Copyright (C) 2017 Jeffrey L. Overbey.  Use of this source code is governed
// by a BSD-style license posted at http://blog.jeff.over.bz/license/

#ifndef X86ASM_H
#define X86ASM_H

#include <stdint.h> // uint8_t, unint32_t

typedef enum { EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI } reg32_t;

uint8_t *nop(uint8_t *buf);

uint8_t *mov_immediate(reg32_t dest, int32_t value, uint8_t *buf);
uint8_t * mov_from_ptr(reg32_t dest, reg32_t src, uint8_t *buf);
uint8_t *   mov_to_ptr(reg32_t dest, reg32_t src, uint8_t *buf);
uint8_t *          mov(reg32_t dest, reg32_t src, uint8_t *buf);

uint8_t *  add(reg32_t dest, reg32_t src, uint8_t *buf);
uint8_t *  sub(reg32_t dest, reg32_t src, uint8_t *buf);
uint8_t *  and(reg32_t dest, reg32_t src, uint8_t *buf);
uint8_t *   or(reg32_t dest, reg32_t src, uint8_t *buf);
uint8_t *  xor(reg32_t dest, reg32_t src, uint8_t *buf);
uint8_t *  cmp(reg32_t dest, reg32_t src, uint8_t *buf);
uint8_t *  inc(reg32_t reg, uint8_t *buf);
uint8_t *  dec(reg32_t reg, uint8_t *buf);
uint8_t *  not(reg32_t reg, uint8_t *buf);
uint8_t *  neg(reg32_t reg, uint8_t *buf);
uint8_t *  mul(reg32_t reg, uint8_t *buf);
uint8_t * imul(reg32_t reg, uint8_t *buf);
uint8_t * div_(reg32_t reg, uint8_t *buf);
uint8_t * idiv(reg32_t reg, uint8_t *buf);
uint8_t *  cdq(uint8_t *buf);

uint8_t *   shl(reg32_t reg, uint8_t bits, uint8_t *buf);
uint8_t *shl_cl(reg32_t reg, uint8_t *buf);
uint8_t *   shr(reg32_t reg, uint8_t bits, uint8_t *buf);
uint8_t *shr_cl(reg32_t reg, uint8_t *buf);
uint8_t *   sar(reg32_t reg, uint8_t bits, uint8_t *buf);
uint8_t *sar_cl(reg32_t reg, uint8_t *buf);

uint8_t *push(reg32_t reg, uint8_t *buf);
uint8_t * pop(reg32_t reg, uint8_t *buf);
uint8_t *call(reg32_t reg, uint8_t *buf);
uint8_t * ret(uint16_t bytes, uint8_t *buf);

uint8_t * jmp(int32_t relative_bytes, uint8_t *buf);
uint8_t *  jb(int32_t relative_bytes, uint8_t *buf);
uint8_t * jae(int32_t relative_bytes, uint8_t *buf);
uint8_t *  je(int32_t relative_bytes, uint8_t *buf);
uint8_t * jne(int32_t relative_bytes, uint8_t *buf);
uint8_t * jbe(int32_t relative_bytes, uint8_t *buf);
uint8_t *  ja(int32_t relative_bytes, uint8_t *buf);
uint8_t *  jl(int32_t relative_bytes, uint8_t *buf);
uint8_t * jge(int32_t relative_bytes, uint8_t *buf);
uint8_t * jle(int32_t relative_bytes, uint8_t *buf);
uint8_t *  jg(int32_t relative_bytes, uint8_t *buf);

#endif

3. The demo program: demo.c

   Before delving into the implementation of the assembler, it's probably
   helpful to show how this API is used.

   Each function in our API takes a uint8_t pointer buf, writes the
   byte(s) of machine code for a single assembly language instruction to
   memory starting at that address, then returns a pointer to the next
   byte after the instruction that was just assembled.

   For example, the instruction mov eax, 12345678h is assembled into five
   bytes of machine code: b8 78 56 34 12. Calling mov_immediate(EAX,
   0x12345678, buf) stores these five bytes into memory at the location
   pointed to by buf, then it returns buf+5, which is presumably where
   you'll want to store the next instruction.

   For example, suppose you want to assemble the following
   three-instruction program.
mov eax, 120h
add eax, ecx
shl eax, 4

   The following program illustrates how to assemble this sequence of
   three instructions, then write the byte values of the resulting machine
   code to standard output:
#include "x86asm.h"
#include <stdio.h>

int main() {
        uint8_t bytes[64];
        uint8_t *cur = bytes;
        cur = mov_immediate(EAX, 0x120, cur);  // mov eax, 120h
        cur = add(EAX, ECX, cur);              // add eax, ecx
        cur = shl(EAX, 4, cur);                // shl eax, 4

        for (uint8_t *p = bytes; p < cur; p++) {
                printf("%02x ", *p);
        }
        printf("\n");
        return 0;
}

   When you run this, the output is:
b8 20 01 00 00 03 c1 c1 e0 04

4. The implementation: x86asm.c

   Now, we'll start implementing this API. For each instruction, I'll
   describe its machine language encoding, and then the C function that
   implements it.

   The definitive, official reference for the x86 instruction set and its
   machine language encoding is Volume 2 of the [12]Intel® 64 and IA-32
   Architectures Software Developer Manuals. Unfortunately, Intel's
   documentation is not easy to read, so for this small assembler, it will
   be sufficient to simply describe the encodings by example.

No operation - nop

   The nop instruction assembles to a single byte of machine code: 90h.
uint8_t *nop(uint8_t *buf) {
        *buf++ = 0x90;
        return buf;
}

Increment and decrement - inc, dec

   The inc instruction adds 1 to a value in a register; dec subtracts 1.
   Recall from the header file above (x86asm.h) that we defined an
   enumeration with all of the x86's 32-bit registers.
typedef enum { EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI } reg32_t;

   There's a reason we listed the registers in this specific order: when
   instructions take register operands, the encodings tend to follow this
   same order. Notice the pattern in the encodings of the inc and dec
   instructions:
   Instruction    Encoding (hex)          Instruction    Encoding (hex)
   inc eax        40                      dec eax        48
   inc ecx        41                      dec ecx        49
   inc edx        42                      dec edx        4A
   ...                                    ...
   inc edi        47                      dec edi        4F

   Since our reg32_t enum assigns an integer value to each register name
   (EAX=0, ECX=1, EDX=2, etc.), this means we can encode inc register by
   simply adding the register number to hexadecimal 40.
uint8_t *inc(reg32_t reg, uint8_t *buf) {
        *buf++ = 0x40 + reg;
        return buf;
}

uint8_t *dec(reg32_t reg, uint8_t *buf) {
        *buf++ = 0x48 + reg;
        return buf;
}

   (It's more conventional to describe encodings in terms of which bits in
   the encoding represent the operand register. For example, see Volume 2,
   Appendix B of the Intel documentation referenced above. From that
   perspective, it might make more sense to build encodings using bitwise
   operations. However, I'm writing this blog post from the perspective of
   "look at the pattern and implement it;" adding values seems more
   intuitive and produces the same result.)

Move immediate value to register - mov reg, imm

   The following table shows the encodings for mov reg, 1 and
   mov reg, 12345678h. Notice the pattern?
   Instruction    Encoding (hex)             Instruction        Encoding (hex)
   mov eax, 1     B8 01 00 00 00          mov eax, 12345678h    B8 78 56 34 12
   mov ecx, 1     B9 01 00 00 00          mov ecx, 12345678h    B9 78 56 34 12
   mov edx, 1     BA 01 00 00 00          mov edx, 12345678h    BA 78 56 34 12
   ...                                    ...
   mov edi, 1     BF 01 00 00 00          mov edi, 12345678h    BF 78 56 34 12

   While the inc and dec instructions had 1-byte encodings, the encoding
   here is always 5 bytes. The first byte of the encoding is B8 + the
   register number. The next four bytes are the immediate value in little
   endian order, i.e., with the low-order byte first. Assuming the
   assembler will be run on an x86/x64 processor, which uses little endian
   byte ordering natively, nothing special needs to be done to reorder the
   bytes--storing a 32-bit value in memory will store the bytes in little
   endian order.
uint8_t *mov_immediate(reg32_t dest, int32_t value, uint8_t *buf) {
        *buf++ = 0xB8 + dest;
        *((int32_t *)buf) = value; buf += sizeof(int32_t);
        return buf;
}

Load value from memory - mov reg, DWORD PTR [reg]

   So far, our instructions have had straightforward encodings with
   reasonably obvious patterns. This one gets a bit more interesting.
         Instruction           Encoding (hex)
   mov eax, DWORD PTR [eax]    8B 00
   mov eax, DWORD PTR [ecx]    8B 01
   mov eax, DWORD PTR [edx]    8B 02
   mov eax, DWORD PTR [ebx]    8B 03
   mov eax, DWORD PTR [esp]    8B 04 24
   mov eax, DWORD PTR [ebp]    8B 45 00
   mov eax, DWORD PTR [esi]    8B 06
   mov eax, DWORD PTR [edi]    8B 07

   mov ecx, DWORD PTR [eax]    8B 08
   mov ecx, DWORD PTR [ecx]    8B 09
   mov ecx, DWORD PTR [edx]    8B 0A
   mov ecx, DWORD PTR [ebx]    8B 0B
   mov ecx, DWORD PTR [esp]    8B 0C 24
   mov ecx, DWORD PTR [ebp]    8B 4D 00
   mov ecx, DWORD PTR [esi]    8B 0E
   mov ecx, DWORD PTR [edi]    8B 0F

   mov edx, DWORD PTR [eax]    8B 10
   mov edx, DWORD PTR [ecx]    8B 11
   ...
   mov edi, DWORD PTR [edi]    8B 3F

   This form of the mov instruction has a two-byte encoding with a fairly
   obvious pattern, except when the source operand is ESP or EBP... then
   it's a three-byte encoding with a not-so-obvious pattern.^1
uint8_t *mov_from_ptr(reg32_t dest, reg32_t src, uint8_t *buf) {
        *buf++ = 0x8B;
        if (src == ESP) {
                *buf++ = 8*dest + src;
                *buf++ = 0x24;
        } else if (src == EBP) {
                *buf++ = 0x45 + 8*dest;
                *buf++ = 0x00;
        } else {
                *buf++ = 8*dest + src;
        }
        return buf;
}

Store value into memory - mov DWORD PTR [reg], reg

   When mov is used to store a value in memory, the encodings are almost
   identical to the encodings for loading a value from memory, except the
   first byte is 89h and the source and destination operands are reversed
   when encoding the second byte.
         Instruction           Encoding (hex)
   mov DWORD PTR [eax], eax    89 00
   mov DWORD PTR [ecx], eax    89 01
   mov DWORD PTR [edx], eax    89 02
   mov DWORD PTR [ebx], eax    89 03
   mov DWORD PTR [esp], eax    89 04 24
   mov DWORD PTR [ebp], eax    89 45 00
   mov DWORD PTR [esi], eax    89 06
   mov DWORD PTR [edi], eax    89 07
   mov DWORD PTR [eax], ecx    89 08
   mov DWORD PTR [ecx], ecx    89 09
   mov DWORD PTR [edx], ecx    89 0A
   mov DWORD PTR [ebx], ecx    89 0B
   mov DWORD PTR [esp], ecx    89 0C 24
   mov DWORD PTR [ebp], ecx    89 4D 00
   mov DWORD PTR [esi], ecx    89 0E
   mov DWORD PTR [edi], ecx    89 0F
   mov DWORD PTR [eax], edx    89 10
   mov DWORD PTR [ecx], edx    89 11
   ...
   mov DWORD PTR [edi], edi    89 3F
uint8_t *mov_to_ptr(reg32_t dest, reg32_t src, uint8_t *buf) {
        *buf++ = 0x89;
        if (dest == ESP) {
                *buf++ = 8*src + dest;
                *buf++ = 0x24;
        } else if (dest == EBP) {
                *buf++ = 0x45 + 8*src;
                *buf++ = 0x00;
        } else {
                *buf++ = 8*src + dest;
        }
        return buf;
}

RM-encoded instructions: mov, add, sub, and, or, xor, cmp

   Next, we will tackle register-register mov, as well as add, sub, and,
   or, xor, and cmp. All of these instructions have a similar encoding: an
   opcode byte (that differs from one instruction to the next - hence the
   name, "operation code"), followed by a single byte indicating the
   source and destination registers.

   To see the pattern, consider mov and add:
   Instruction     Encoding (hex)          Instruction     Encoding (hex)
   mov eax, eax    8B C0                   add eax, eax    03 C0
   mov eax, ecx    8B C1                   add eax, ecx    03 C1
   mov eax, edx    8B C2                   add eax, edx    03 C2
   ...                                     ...
   mov eax, edi    8B C7                   add eax, edi    03 C7
   mov ecx, eax    8B C8                   add ecx, eax    03 C8
   mov ecx, ecx    8B C9                   add ecx, ecx    03 C9
   ...                                     ...
   mov ecx, edi    8B CF                   add ecx, edi    03 CF
   mov edx, eax    8B D0                   add edx, eax    03 D0
   ...                                     ...
   mov edi, edi    8B FF                   add edi, edi    03 FF

   The second byte of the encoding is hex C0, plus 8 times the destination
   register number, plus the source register number.
#define DEFINE_INSN_RM(mnemonic, opcode)                     \
uint8_t *mnemonic(reg32_t dest, reg32_t src, uint8_t *buf) { \
        *buf++ = opcode;                                     \
        *buf++ = 8*dest + 0xC0 + src;                        \
        return buf;                                          \
}

DEFINE_INSN_RM(mov, 0x8B)
DEFINE_INSN_RM(add, 0x03)
DEFINE_INSN_RM(sub, 0x2B)
DEFINE_INSN_RM(and, 0x23)
DEFINE_INSN_RM( or, 0x0B)
DEFINE_INSN_RM(xor, 0x33)
DEFINE_INSN_RM(cmp, 0x3B)

Instructions with opcodes beginning with F7: not, neg, mul, imul, div, idiv

   The not, neg, mul, imul, div, and idiv instructions also have similar
   encodings. The first byte of the encoding is F7. The second byte
   indicates both the operation and the operand (register).
   Instruction    Encoding (hex)          Instruction    Encoding (hex)
   not eax        F7 D0                   neg eax        F7 D8
   not ecx        F7 D1                   neg ecx        F7 D9
   ...
   not edi        F7 D7                   neg edi        F7 DF

   As a note, we named the C function for the div instruction div_, since
   the C standard library's stdlib.h includes the [13]div(3) instruction.
#define DEFINE_INSN_F7(mnemonic, reg_base)     \
uint8_t *mnemonic(reg32_t reg, uint8_t *buf) { \
        *buf++ = 0xF7;                         \
        *buf++ = reg_base + reg;               \
        return buf;                            \
}

DEFINE_INSN_F7( not, 0xD0)
DEFINE_INSN_F7( neg, 0xD8)
DEFINE_INSN_F7( mul, 0xE0)
DEFINE_INSN_F7(imul, 0xE8)
DEFINE_INSN_F7(div_, 0xF0)
DEFINE_INSN_F7(idiv, 0xF8)

Convert doubleword to quadword - cdq

   Both the div and idiv instructions take a 64-bit dividend (with the
   high 32 bits in EDX and the low 32 bits in EAX) and divide it by a
   32-bit divisor (the register operand). To divide two 32-bit values, the
   dividend must be extended to 64 bits. For unsigned division (div), this
   is easy: mov edx, 0. For signed division (idiv), the 32-bit value must
   be sign-extended to 64 bits. This is done by the cdq instruction: it
   copies the sign bit of EAX into all 32 bits of EDX.
uint8_t *cdq(uint8_t *buf) {
        *buf++ = 0x99;
        return buf;
}

Bit shift instructions - shl, shr, sar

   The bit shift instructions are interesting for two reasons:
     * The number of bits to shift can be an immediate value (0-255), or
       it can be stored in the CL register (another name for the lowest 8
       bits of the ECX register).
     * The encoding for a one-bit shift is different.

   Using the left shift instruction as an example:
    Instruction     Encoding (hex)          Instruction    Encoding (hex)
   shl eax, 0       C1 E0 00                shl eax, cl    D3 E0
   shl eax, 1       D1 E0                   shl ecx, cl    D3 E1
   shl eax, 2       C1 E0 02                shl edx, cl    D3 E2
   shl eax, 3       C1 E0 03                shl ebx, cl    D3 E3
   ...                                      ...
   shl ecx, 0FFh    C1 E1 FF
   shl ecx, 0       C1 E1 00
   shl ecx, 1       D1 E1
   shl ecx, 2       C1 E1 02
   shl ecx, 3       C1 E1 03
   ...
   shl ecx, 0FFh    C1 E1 FF
   ...

   We can implement this in our assembler as follows.
#define DEFINE_INSN_D1C1(mnemonic, reg_base)                  \
uint8_t *mnemonic(reg32_t reg, uint8_t bits, uint8_t *buf) {  \
        switch (bits) {                                       \
        case 1: /* 1-bit shifts have a different opcode */    \
                *buf++ = 0xD1;                                \
                *buf++ = reg_base + reg;                      \
                break;                                        \
        default:                                              \
                *buf++ = 0xC1;                                \
                *buf++ = reg_base + reg;                      \
                *buf++ = bits;                                \
        }                                                     \
        return buf;                                           \
}                                                             \
uint8_t *mnemonic##_cl(reg32_t reg, uint8_t *buf) {           \
        *buf++ = 0xD3;                                        \
        *buf++ = reg_base + reg;                              \
        return buf;                                           \
}

DEFINE_INSN_D1C1(shl, 0xE0)
DEFINE_INSN_D1C1(shr, 0xE8)
DEFINE_INSN_D1C1(sar, 0xF8)

Procedure calls: push, pop, call, ret

   The push, pop, call, and ret instructions are the four essential
   instructions for procedure calls. Their encodings follow similar
   patterns to those we've already seen, except with different opcode
   bytes.
uint8_t *push(reg32_t reg, uint8_t *buf) {
        *buf++ = 0x50 + reg;
        return buf;
}

uint8_t *pop(reg32_t reg, uint8_t *buf) {
        *buf++ = 0x58 + reg;
        return buf;
}

uint8_t *call(reg32_t reg, uint8_t *buf) {
        *buf++ = 0xFF;
        *buf++ = 0xD0 + reg;
        return buf;
}

   The encoding of ret is only slightly more interesting, since ret 0
   (which is often written as ret with no operand) is encoded differently
   than ret with a nonzero immediate operand, such as ret 4 or ret 16.
uint8_t *ret(uint16_t bytes, uint8_t *buf) {
        if (bytes == 0) {
                *buf++ = 0xC3;
        } else {
                *buf++ = 0xC2;
                *((uint16_t *)buf) = bytes; buf += sizeof(uint16_t);
        }
        return buf;
}

Jumps

   In x86 assembly language, jumps are usually written with labels. For
   example:
there: mov eax, 12345678h    ; b8 78 56 34 12
       jmp there             ; eb f9
       nop                   ; 90

   Recall that the EIP register is the instruction pointer. When the
   processor fetches an instruction to execute, it increments EIP to point
   to the following instruction. A jump changes the value of EIP. In our
   example, the effect of the jump is to move EIP backward by 7 bytes, so
   it will point to the start of the mov instruction.
                            EIP is here after the processor
                            fetches the  "jmp there" instruction
                            |v
B8  78  56  34  12  EB  F9  90
^|___________________________|
We want to move it 7 bytes backward
to place it here

   So, how is jmp encoded? Hex F9 is the two's complement representation
   of -7... so the encoding above (EB F9) is in essence "jump -7 bytes."

   Complicating things slightly, the jmp instruction is encoded with an EB
   opcode byte if the jump distance is between -128 and 127 bytes,
   inclusive, and with an E9 opcode if the jump distance is larger than
   that.
uint8_t *jmp(int32_t bytes, uint8_t *buf) {
        if (INT8_MIN <= bytes && bytes <= INT8_MAX) {
                *buf++ = 0xEB;
                *buf++ = (int8_t)bytes;
        } else {
                *buf++ = 0xE9;
                *((int32_t *)buf) = bytes; buf += sizeof(int32_t);
        }
        return buf;
}

   Conditional jumps are encoded similarly, except with different opcodes,
   of course.
#define DEFINE_INSN_JCC(mnemonic, byte_opcode)                     \
uint8_t *mnemonic(int32_t bytes, uint8_t *buf) {                   \
        if (INT8_MIN <= bytes && bytes <= INT8_MAX) {              \
                *buf++ = byte_opcode;                              \
                *buf++ = (int8_t)bytes;                            \
        } else {                                                   \
                *buf++ = 0x0F;                                     \
                *buf++ = byte_opcode + 0x10;                       \
                *((int32_t *)buf) = bytes; buf += sizeof(int32_t); \
        }                                                          \
        return buf;                                                \
}

DEFINE_INSN_JCC( jb, 0x72)
DEFINE_INSN_JCC(jae, 0x73)
DEFINE_INSN_JCC( je, 0x74)
DEFINE_INSN_JCC(jne, 0x75)
DEFINE_INSN_JCC(jbe, 0x76)
DEFINE_INSN_JCC( ja, 0x77)
DEFINE_INSN_JCC( jl, 0x7C)
DEFINE_INSN_JCC(jge, 0x7D)
DEFINE_INSN_JCC(jle, 0x7E)
DEFINE_INSN_JCC( jg, 0x7F)

5. What's next?

   So, we have a working x86 assembler. Not bad for 256 lines of code. You
   can download the complete source code below.

   In the next few posts, we'll:
     * show how to test this assembler (are you sure it actually works?).
     * show how to find the encodings of other instructions (in case you
       want to extend this assembler).
     * show how to actually execute the generated machine code.

   At some point in the future - maybe not right away - I'd like to
     * show how the Builder design pattern can make the assembler easier
       to use.
     * build an x64 assembler (since you're probably not running a 32-bit
       machine).

   But there are plenty of other non-assembler-related topics I'd like to
   blog about, so let's see what actually materializes.

Download the source code

   Source Code:    [14]x86asm.h      69 lines
                   [15]x86asm.c      171 lines
                   [16]demo.c        16 lines
                                     Total: 256 lines
   Makefiles:      [17]GNUmakefile   (GNU Make on Linux/macOS)
                   [18]Makefile      (NMAKE on Windows)

   ^1 If you're familiar with the x86 encoding scheme, [EBP] is actually
   encoded as [EBP+0] (i.e., EBP with an 8-bit displacement), and ESP is
   encoded using the SIB byte.

   Published on 15 Jan 2017 o 4019 words o Comments? [19]E-mail me!

   Copyright © 2017 Jeffrey L. Overbey. All rights reserved. Except for
   source code where an explicit license is given, no part of this blog
   may be copied, reproduced, published, translated, or distributed, in
   whole or in part, without the written permission of the copyright
   owner.

References

   1. http://blog.jeff.over.bz/rss.xml
   2. http://jeff.over.bz/
   3. http://blog.jeff.over.bz/refactoring/golang/godoctor/2018/04/22/building-a-godoctor-refactoring.html
   4. http://blog.jeff.over.bz/compilers/lexers/2017/09/09/lexical-analysis.html
   5. http://blog.jeff.over.bz/performance/statistics/2017/06/01/on-performance-improvements.html
   6. http://blog.jeff.over.bz/assembly/compilers/jit/2017/03/30/executing-dynamically-generated-machine-code.html
   7. http://blog.jeff.over.bz/assembly/2017/02/15/finding-machine-language-encodings.html
   8. http://blog.jeff.over.bz/assembly/compilers/jit/2017/01/15/x86-assembler.html
   9. http://blog.jeff.over.bz/blog/2016/12/15/first-post.html
  10. http://blog.jeff.over.bz/rss.xml
  11. http://jeff.over.bz/
  12. https://software.intel.com/en-us/articles/intel-sdm
  13. https://linux.die.net/man/3/div
  14. http://blog.jeff.over.bz/_posts/code/x86-assembler/x86asm.h
  15. http://blog.jeff.over.bz/_posts/code/x86-assembler/x86asm.c
  16. http://blog.jeff.over.bz/_posts/code/x86-assembler/demo.c
  17. http://blog.jeff.over.bz/_posts/code/x86-assembler/GNUmakefile
  18. http://blog.jeff.over.bz/_posts/code/x86-assembler/Makefile
  19. http://www.google.com/recaptcha/mailhide/d?k=01Y7tF2jw9w9xLZucF314wPA==&c=q6vUJyh7NztFNzwTojRuECzwRgJ8lixJT3LvIi58VCM=