https://github.com/akkartik/mu/blob/master/069allocate.subx
  1 # Helper to dynamically allocate memory on the heap.
  2 #
  3 # We'd like to be able to write tests for functions that allocate memory,
  4 # making assertions on the precise addresses used. To achieve this we'll pass
  5 # in an *allocation descriptor* to allocate from.
  6 #
  7 # Allocation descriptors are also useful outside of tests. Assembly and machine
  8 # code are of necessity unsafe languages, and one of the most insidious kinds
  9 # of bugs unsafe languages expose us to are dangling pointers to memory that
 10 # has been freed and potentially even reused for something totally different.
 11 # To reduce the odds of such "use after free" errors, SubX programs tend to not
 12 # reclaim and reuse dynamically allocated memory. (Running out of memory is far
 13 # easier to debug.) Long-running programs that want to reuse memory are mostly
 14 # on their own to be careful. However, they do get one bit of help: they can
 15 # carve out chunks of memory and then allocate from them manually using this
 16 # very same 'allocate' helper. They just need a new allocation descriptor for
 17 # their book-keeping.
 18 
 19 == code
 20 #   instruction                     effective address                                                   register    displacement    immediate
 21 # . op          subop               mod             rm32          base        index         scale       r32
 22 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 23 
 24 # Claim the next 'n' bytes of memory starting at ad->curr and update ad->curr.
 25 # Abort if there isn't enough memory in 'ad'.
 26 allocate:  # ad : (address allocation-descriptor), n : int -> address-or-null/EAX
 27     # . prolog
 28     55/push-EBP
 29     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
 30     # . save registers
 31     51/push-ECX
 32     52/push-EDX
 33     # ECX = ad
 34     8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
 35     # save ad->curr
 36     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
 37     # check if there's enough space
 38     # . EDX = ad->curr + n
 39     89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EDX
 40     03/add                          1/mod/*+disp8   5/rm32/EBP    .           .             .           2/r32/EDX   0xc/disp8       .                 # add *(EBP+12) to EDX
 41     3b/compare                      1/mod/*+disp8   1/rm32/ECX    .           .             .           2/r32/EDX   4/disp8         .                 # compare EDX with *(ECX+4)
 42     73/jump-if-greater-or-equal-signed  $allocate:abort/disp8
 43 $allocate:commit:
 44     # update ad->curr
 45     89/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # copy EDX to *ECX
 46 $allocate:end:
 47     # . restore registers
 48     5a/pop-to-EDX
 49     59/pop-to-ECX
 50     # . epilog
 51     89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
 52     5d/pop-to-EBP
 53     c3/return
 54 
 55 $allocate:abort:
 56     # . _write(2/stderr, error)
 57     # . . push args
 58     68/push  "allocate: failed to allocate\n"/imm32
 59     68/push  2/imm32/stderr
 60     # . . call
 61     e8/call  _write/disp32
 62     # . . discard args
 63     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 64     # . syscall(exit, 1)
 65     bb/copy-to-EBX  1/imm32
 66     b8/copy-to-EAX  1/imm32/exit
 67     cd/syscall  0x80/imm8
 68     # never gets here
 69 
 70 test-allocate-success:
 71     # . prolog
 72     55/push-EBP
 73     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
 74     # var ad/ECX : (address allocation-descriptor) = {11, 15}
 75     68/push  0xf/imm32/limit
 76     68/push  0xb/imm32/curr
 77     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
 78     # EAX = allocate(ad, 3)
 79     # . . push args
 80     68/push  3/imm32
 81     51/push-ECX
 82     # . . call
 83     e8/call  allocate/disp32
 84     # . . discard args
 85     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 86     # check-ints-equal(EAX, 11, msg)
 87     # . . push args
 88     68/push  "F - test-allocate-success: returns current pointer of allocation descriptor"/imm32
 89     68/push  0xb/imm32
 90     50/push-EAX
 91     # . . call
 92     e8/call  check-ints-equal/disp32
 93     # . . discard args
 94     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
 95     # check-ints-equal(ad->curr, 14, msg)
 96     # . . push args
 97     68/push  "F - test-allocate-success: updates allocation descriptor"/imm32
 98     68/push  0xe/imm32
 99     ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
100     # . . call
101     e8/call  check-ints-equal/disp32
102     # . . discard args
103     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
104     # . epilog
105     89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
106     5d/pop-to-EBP
107     c3/return
108 
109 _pending-test-allocate-failure:
110     # . prolog
111     55/push-EBP
112     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
113     # var ad/ECX : (address allocation-descriptor) = {11, 15}
114     68/push  0xf/imm32/limit
115     68/push  0xb/imm32/curr
116     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
117     # EAX = allocate(ad, 6)
118     # . . push args
119     68/push  6/imm32
120     51/push-ECX
121     # . . call
122     e8/call  allocate/disp32
123     # . . discard args
124     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
125     # check-ints-equal(EAX, 0, msg)
126     # . . push args
127     68/push  "F - test-allocate-failure: returns null"/imm32
128     68/push  0/imm32
129     50/push-EAX
130     # . . call
131     e8/call  check-ints-equal/disp32
132     # . . discard args
133     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
134     # no change to ad->curr
135     # . check-ints-equal(ad->curr, 11)
136     # . . push args
137     68/push  "F - test-allocate-failure: updates allocation descriptor"/imm32
138     68/push  0xb/imm32
139     ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
140     # . . call
141     e8/call  check-ints-equal/disp32
142     # . . discard args
143     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
144     # . epilog
145     89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
146     5d/pop-to-EBP
147     c3/return
148 
149 # helper: create a nested allocation descriptor (useful for tests)
150 allocate-region:  # ad : (address allocation-descriptor), n : int -> new-ad : (address allocation-descriptor)
151     # . prolog
152     55/push-EBP
153     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
154     # . save registers
155     51/push-ECX
156     # EAX = allocate(ad, n)
157     # . . push args
158     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
159     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
160     # . . call
161     e8/call  allocate/disp32
162     # . . discard args
163     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
164     # if (EAX == 0) abort
165     3d/compare-EAX-and  0/imm32
166     74/jump-if-equal  $allocate-region:abort/disp8
167     # earmark 8 bytes at the start for a new allocation descriptor
168     # . *EAX = EAX + 8
169     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to ECX
170     81          0/subop/add         3/mod/direct    1/rm32/ECX    .           .             .           .           .               8/imm32           # add to ECX
171     89/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EAX
172     # . *(EAX+4) = EAX + n
173     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to ECX
174     03/add                          1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   0xc/disp8       .                 # add *(EBP+12) to ECX
175     89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           1/r32/ECX   4/disp8         .                 # copy ECX to *(EAX+4)
176     # . restore registers
177     59/pop-to-ECX
178     # . epilog
179     89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
180     5d/pop-to-EBP
181     c3/return
182 
183 # We could create a more general '$abort' jump target, but then we'd need to do
184 # a conditional jump followed by loading the error message and an unconditional
185 # jump. Or we'd need to unconditionally load the error message before a
186 # conditional jump, even if it's unused the vast majority of the time. This way
187 # we bloat a potentially cold segment in RAM so we can abort with a single
188 # instruction.
189 $allocate-region:abort:
190     # . _write(2/stderr, error)
191     # . . push args
192     68/push  "allocate-region: failed to allocate\n"/imm32
193     68/push  2/imm32/stderr
194     # . . call
195     e8/call  _write/disp32
196     # . . discard args
197     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
198     # . syscall(exit, 1)
199     bb/copy-to-EBX  1/imm32
200     b8/copy-to-EAX  1/imm32/exit
201     cd/syscall  0x80/imm8
202     # never gets here
203 
204 # . . vim:nowrap:textwidth=0