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