about summary refs log tree commit diff stats
path: root/120allocate.subx
blob: fd5b5490747fed490c405c5f3da95bf4affa504c (plain) (blame)
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
ref='#n354'>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 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
# Helper to dynamically allocate memory on the heap.
#
# We'd like to be able to write tests for functions that allocate memory,
# making assertions on the precise addresses used. To achieve this we'll pass
# in an *allocation descriptor* to allocate from.
#
# Allocation descriptors are also useful outside of tests. Assembly and machine
# code are of necessity unsafe languages, and one of the most insidious kinds
# of bugs unsafe languages expose us to are dangling pointers to memory that
# has been freed and potentially even reused for something totally different.
# To reduce the odds of such "use after free" errors, SubX programs tend to not
# reclaim and reuse dynamically allocated memory. (Running out of memory is far
# easier to debug.) Long-running programs that want to reuse memory are mostly
# on their own to be careful. However, they do get one bit of help: they can
# carve out chunks of memory and then allocate from them manually using this
# very same 'allocate' helper. They just need a new allocation descriptor for
# their book-keeping.

== data

# Allocations are returned in a handle, which consists of an alloc-id and a payload.
# The alloc-id helps detect use-after-free errors.
Handle-size:  # (addr int)
  8/imm32

# A default allocation descriptor for programs to use.
Heap:  # allocation-descriptor
  # curr
  0x02000000/imm32  # 32 MB
  # limit
  0x80000000/imm32  # 2 GB

Next-alloc-id:  # int
  0x100/imm32  # save a few alloc ids for fake handles

== code
#   instruction                     effective address                                                   register    displacement    immediate
# . op          subop               mod             rm32          base        index         scale       r32
# . 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

# Allocate and clear 'n' bytes of memory from an allocation-descriptor 'ad'.
# Abort if there isn't enough memory in 'ad'.
allocate:  # ad: (addr allocation-descriptor), n: int, out: (addr handle _)
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    50/push-eax
    # allocate-raw(ad, n, out)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x10/disp8      .                 # push *(ebp+16)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
    # . . call
    e8/call  allocate-raw/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # eax = out->payload + 4
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0x10/disp8      .                 # copy *(ebp+16) to eax
    8b/copy                         1/mod/*+disp8   0/rm32/eax    .           .             .           0/r32/eax   4/disp8         .                 # copy *(eax+4) to eax
    05/add-to-eax  4/imm32
    # zero-out(eax, n)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    50/push-eax
    # . . call
    e8/call  zero-out/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
$allocate:end:
    # . restore registers
    58/pop-to-eax
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# Claim the next 'n' bytes of memory starting at ad->curr and update ad->curr.
# Abort if there isn't enough memory in 'ad'.
allocate-raw:  # ad: (addr allocation-descriptor), n: int, out: (addr handle _)
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    53/push-ebx
    56/push-esi
    57/push-edi
    # ecx = ad
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   8/disp8         .                 # copy *(ebp+8) to ecx
    # edx = out
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           2/r32/edx   0x10/disp8      .                 # copy *(ebp+16) to edx
    # ebx = n
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           3/r32/ebx   0xc/disp8       .                 # copy *(ebp+12) to ebx
    # out->alloc-id = Next-alloc-id
    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/eax   Next-alloc-id/disp32              # copy *Next-alloc-id to eax
    89/copy                         0/mod/indirect  2/rm32/edx    .           .             .           0/r32/eax   .               .                 # copy eax to *edx
    # out->payload = ad->curr
    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy *ecx to eax
$allocate-raw:save-payload-in-eax:
    89/copy                         1/mod/*+disp8   2/rm32/edx    .           .             .           0/r32/eax   4/disp8         .                 # copy eax to *(edx+4)
    # *out->payload = Next-alloc-id
    8b/copy                         1/mod/*+disp8   2/rm32/edx    .           .             .           7/r32/edi   4/disp8         .                 # copy *(edx+4) to edi
    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           6/r32/esi   Next-alloc-id/disp32              # copy *Next-alloc-id to esi
    89/copy                         0/mod/indirect  7/rm32/edi    .           .             .           6/r32/esi   .               .                 # copy esi to *edi
$allocate-raw:increment-next-alloc-id:
    # increment *Next-alloc-id
    ff          0/subop/increment   0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32              # increment *Next-alloc-id
    # check if there's enough space
    # TODO: move this check up before any state updates when we support error recovery
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  3/index/ebx   .           0/r32/eax   4/disp8         .                 # copy eax+ebx+4 to eax
    3b/compare                      1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   4/disp8         .                 # compare eax with *(ecx+4)
    73/jump-if->=-signed  $allocate-raw:abort/disp8
$allocate-raw:commit:
    # ad->curr += n+4
    89/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy eax to *ecx
$allocate-raw:end:
    # . restore registers
    5f/pop-to-edi
    5e/pop-to-esi
    5b/pop-to-ebx
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$allocate-raw:abort:
    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "allocate: failed" 3 0)  # 3=cyan
    {
      eb/jump loop/disp8
    }
    # never gets here

test-allocate-raw-success:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var ad/ecx: allocation-descriptor containing 16 bytes
    # . var end/ecx: (addr byte)
    89/<- %ecx 4/r32/esp
    # . var start/edx: (addr byte) = end - 16
    81 5/subop/subtract %esp 0x10/imm32
    89/<- %edx 4/r32/esp
    # . ad = {start, end}
    51/push-ecx
    52/push-edx
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var expected-payload/ebx = ad->curr
    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           3/r32/ebx   .               .                 # copy *ecx to ebx
    # var h/edx: handle = {0, 0}
    68/push  0/imm32
    68/push  0/imm32
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # *Next-alloc-id = 0x34
    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32  0x34/imm32  # copy to *Next-alloc-id
    # allocate-raw(ad, 3, h)
    # . . push args
    52/push-edx
    68/push  3/imm32
    51/push-ecx
    # . . call
    e8/call  allocate-raw/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->alloc-id, 0x34, msg)
    # . . push args
    68/push  "F - test-allocate-raw-success: sets alloc-id in handle"/imm32
    68/push  0x34/imm32
    ff          6/subop/push        0/mod/indirect  2/rm32/edx    .           .             .           .           .               .                 # push *edx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->payload, expected-payload, msg)
    # . . push args
    68/push  "F - test-allocate-raw-success: sets payload in handle"/imm32
    53/push-ebx
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->payload->alloc-id, 0x34, msg)
    # . . push args
    68/push  "F - test-allocate-raw-success: sets alloc-id in payload"/imm32
    68/push  0x34/imm32
    ff          6/subop/push        0/mod/indirect  3/rm32/ebx    .           .             .           .           .               .                 # push *ebx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(*Next-alloc-id, 0x35, msg)
    # . . push args
    68/push  "F - test-allocate-raw-success: increments Next-alloc-id"/imm32
    68/push  0x35/imm32
    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32              # push *Next-alloc-id
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(ad->curr - expected-payload, 3 + 4 for alloc-id, msg)
    # . . push args
    68/push  "F - test-allocate-raw-success: updates allocation descriptor"/imm32
    68/push  7/imm32
    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy *ecx to eax
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           3/r32/ebx   .               .                 # subtract ebx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # clean up
    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32  0x100/imm32 # copy to *Next-alloc-id
    # . reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x20/imm32        # add to esp
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

lookup:  # h: (handle _T) -> result/eax: (addr _T)
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    # eax = 0
    31/xor                          3/mod/direct    0/rm32/eax    .           .             .           0/r32/eax   .               .                 # clear eax
    # ecx = handle->alloc_id
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   8/disp8         .                 # copy *(ebp+8) to ecx
    # if (ecx == 0) return 0
    81          7/subop/compare     3/mod/direct    1/rm32/ecx    .           .             .           .           .               0/imm32           # compare ecx
    74/jump-if-=  $lookup:end/disp8
    # eax = handle->address (payload)
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0xc/disp8       .                 # copy *(ebp+12) to eax
    # if (ecx != *eax) abort
    39/compare                      0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # compare *eax and ecx
    75/jump-if-!=  $lookup:abort/disp8
    # add 4 to eax
    05/add-to-eax  4/imm32
$lookup:end:
    # . restore registers
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$lookup:abort:
    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "lookup: failed" 3 0)  # 3=cyan
    {
      eb/jump loop/disp8
    }
    # never gets here

test-lookup-success:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var ad/ebx: allocation-descriptor containing 16 bytes
    # . var end/ecx: (addr byte)
    89/<- %ecx 4/r32/esp
    # . var start/edx: (addr byte) = end - 16
    81 5/subop/subtract %esp 0x10/imm32
    89/<- %edx 4/r32/esp
    # . ad = {start, end}
    51/push-ecx
    52/push-edx
    89/copy                         3/mod/direct    3/rm32/ebx    .           .             .           4/r32/esp   .               .                 # copy esp to ebx
    # var handle/ecx: handle
    68/push  0/imm32/address
    68/push  0/imm32/alloc-id
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var old-top/edx = ad->curr
    8b/copy                         0/mod/indirect  3/rm32/ebx    .           .             .           2/r32/edx   .               .                 # copy *ebx to edx
    # allocate-raw(ad, 2, handle)
    # . . push args
    51/push-ecx
    68/push  2/imm32/size
    53/push-ebx
    # . . call
    e8/call  allocate-raw/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # eax = lookup(handle)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # eax contains old top of heap, except skipping the alloc-id in the payload
    # . check-ints-equal(eax, old-top+4, msg)
    # . . push args
    68/push  "F - test-lookup-success"/imm32
    81          0/subop/add         3/mod/direct    2/rm32/edx    .           .             .           .           .               4/imm32           # add to edx
    52/push-edx
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # clean up
    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32  0x100/imm32 # copy to *Next-alloc-id
    # . reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x20/imm32        # add to esp
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

test-lookup-null-returns-null:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var handle/ecx: handle
    68/push  0/imm32/address
    68/push  0/imm32/alloc-id
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # eax = lookup(handle)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-ints-equal(eax, 0, msg)
    # . . push args
    68/push  "F - test-lookup-null-returns-null"/imm32
    68/push  0/imm32
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # . reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

_pending-test-lookup-failure:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var ad/ecx: allocation-descriptor containing 16 bytes
    # . var end/ecx: (addr byte)
    89/<- %ecx 4/r32/esp
    # . var start/edx: (addr byte) = end - 16
    81 5/subop/subtract %esp 0x10/imm32
    89/<- %edx 4/r32/esp
    # . ad = {start, end}
    51/push-ecx
    52/push-edx
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var h1/ecx: handle
    68/push  0/imm32/address
    68/push  0/imm32/alloc-id
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var old_top/ebx = ad->curr
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           3/r32/ebx   .               .                 # copy *esi to ebx
    # first allocation, to h1
    # . allocate(ad, 2, h1)
    # . . push args
    51/push-ecx
    68/push  2/imm32/size
    56/push-esi
    # . . call
    e8/call  allocate/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # reset ad->curr to mimic reclamation
    89/copy                         0/mod/indirect  6/rm32/esi    .           .             .           3/r32/ebx   .               .                 # copy ebx to *esi
    # second allocation that returns the same address as the first
    # var h2/edx: handle
    68/push  0/imm32/address
    68/push  0/imm32/alloc-id
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # . allocate(ad, 2, h2)
    # . . push args
    52/push-edx
    68/push  2/imm32/size
    56/push-esi
    # . . call
    e8/call  allocate/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h1->address, h2->address, msg)
    # . . push args
    68/push  "F - test-lookup-failure"/imm32
    ff          6/subop/push        1/mod/*+disp8   2/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # lookup(h1) should crash
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # should never get past this point
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # clean up
    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32  0x100/imm32 # copy to *Next-alloc-id
    # . reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x20/imm32        # add to esp
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# when comparing handles, just treat them as pure values
handle-equal?:  # a: (handle _T), b: (handle _T) -> result/eax: boolean
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    # eax = false
    b8/copy-to-eax  0/imm32/false
$handle-equal?:compare-alloc-id:
    # ecx = a->alloc_id
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   8/disp8         .                 # copy *(ebp+8) to ecx
    # if (ecx != b->alloc_id) return false
    39/compare                      1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # compare ecx and *(ebp+16)
    75/jump-if-!=  $handle-equal?:end/disp8
$handle-equal?:compare-address:
    # ecx = handle->address
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy *(ebp+12) to ecx
    # if (ecx != b->address) return false
    39/compare                      1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x14/disp8      .                 # compare ecx and *(ebp+20)
    75/jump-if-!=  $handle-equal?:end/disp8
$handle-equal?:return-true:
    # return true
    b8/copy-to-eax  1/imm32/true
$handle-equal?:end:
    # . restore registers
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

copy-handle:  # src: (handle _T), dest: (addr handle _T)
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    50/push-eax
    51/push-ecx
    # ecx = dest
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # copy *(ebp+16) to ecx
    # *dest = src
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   8/disp8         .                 # copy *(ebp+8) to eax
    89/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy eax to *ecx
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0xc/disp8       .                 # copy *(ebp+12) to eax
    89/copy                         1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   4/disp8         .                 # copy eax to *(ecx+4)
$copy-handle:end:
    # . restore registers
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# helper: create a nested allocation descriptor (useful for tests)
allocate-region:  # ad: (addr allocation-descriptor), n: int, out: (addr handle allocation-descriptor)
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    50/push-eax
    51/push-ecx
    # allocate(ad, n, out)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x10/disp8      .                 # push *(ebp+16)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
    # . . call
    e8/call  allocate/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # eax = out->payload
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0x10/disp8      .                 # copy *(ebp+16) to eax
    8b/copy                         1/mod/*+disp8   0/rm32/eax    .           .             .           0/r32/eax   4/disp8         .                 # copy *(eax+4) to eax
    # skip payload->allocid
    05/add-to-eax  4/imm32
    # if (eax == 0) abort
    3d/compare-eax-and  0/imm32
    74/jump-if-=  $allocate-region:abort/disp8
    # earmark 8 bytes at the start for a new allocation descriptor
    # . *eax = eax + 8
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy eax to ecx
    81          0/subop/add         3/mod/direct    1/rm32/ecx    .           .             .           .           .               8/imm32           # add to ecx
    89/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy ecx to *eax
    # . *(eax+4) = eax + n
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy eax to ecx
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0xc/disp8       .                 # add *(ebp+12) to ecx
    89/copy                         1/mod/*+disp8   0/rm32/eax    .           .             .           1/r32/ecx   4/disp8         .                 # copy ecx to *(eax+4)
    # . restore registers
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# We could create a more general '$abort' jump target, but then we'd need to do
# a conditional jump followed by loading the error message and an unconditional
# jump. Or we'd need to unconditionally load the error message before a
# conditional jump, even if it's unused the vast majority of the time. This way
# we bloat a potentially cold segment in RAM so we can abort with a single
# instruction.
$allocate-region:abort:
    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "allocate-region: failed to allocate" 3 0)  # 3=cyan
    {
      eb/jump loop/disp8
    }
    # never gets here

# Claim the next 'n+4' bytes of memory and initialize the first 4 to n.
# Abort if there isn't enough memory in 'ad'.
allocate-array:  # ad: (addr allocation-descriptor), n: int, out: (addr handle _)
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    # ecx = n
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy *(ebp+12) to ecx
    # var size/edx: int = n+4
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           2/r32/edx   4/disp8         .                 # copy ecx+4 to edx
    # allocate(ad, size, out)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x10/disp8      .                 # push *(ebp+16)
    52/push-edx
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
    # . . call
    e8/call  allocate/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # *out->payload = n
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0x10/disp8      .                 # copy *(ebp+16) to eax
    8b/copy                         1/mod/*+disp8   0/rm32/eax    .           .             .           0/r32/eax   4/disp8         .                 # copy *(eax+4) to eax
    # . skip payload->allocid
    05/add-to-eax  4/imm32
    # .
    89/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy ecx to *eax
$allocate-array:end:
    # . restore registers
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

test-allocate-array:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var ad/ecx: allocation-descriptor containing 16 bytes
    # . var end/ecx: (addr byte)
    89/<- %ecx 4/r32/esp
    # . var start/edx: (addr byte) = end - 16
    81 5/subop/subtract %esp 0x10/imm32
    89/<- %edx 4/r32/esp
    # . ad = {start, end}
    51/push-ecx
    52/push-edx
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var expected-payload/ebx = ad->curr
    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           3/r32/ebx   .               .                 # copy *ecx to ebx
    # var h/edx: handle = {0, 0}
    68/push  0/imm32
    68/push  0/imm32
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # *Next-alloc-id = 0x34
    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32  0x34/imm32  # copy to *Next-alloc-id
    # allocate-array(ad, 3, h)
    # . . push args
    52/push-edx
    68/push  3/imm32
    51/push-ecx
    # . . call
    e8/call  allocate-array/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->alloc-id, 0x34, msg)
    # . . push args
    68/push  "F - test-allocate-array: sets alloc-id in handle"/imm32
    68/push  0x34/imm32
    ff          6/subop/push        0/mod/indirect  2/rm32/edx    .           .             .           .           .               .                 # push *edx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->payload, expected-payload, msg)
    # . . push args
    68/push  "F - test-allocate-array: sets payload in handle"/imm32
    53/push-ebx
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->payload->alloc-id, 0x34, msg)
    # . . push args
    68/push  "F - test-allocate-array: sets alloc-id in payload"/imm32
    68/push  0x34/imm32
    ff          6/subop/push        0/mod/indirect  3/rm32/ebx    .           .             .           .           .               .                 # push *ebx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->payload->size, 3, msg)
    # . . push args
    68/push  "F - test-allocate-array: sets array size in payload"/imm32
    68/push  3/imm32
    ff          6/subop/push        1/mod/*+disp8   3/rm32/ebx    .           .             .           .           4/disp8         .                 # push *(ebx+4)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(*Next-alloc-id, 0x35, msg)
    # . . push args
    68/push  "F - test-allocate-array: increments Next-alloc-id"/imm32
    68/push  0x35/imm32
    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32              # push *Next-alloc-id
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(ad->curr - expected-payload, 3 + 4 for alloc-id + 4 for array size, msg)
    # . . push args
    68/push  "F - test-allocate-array: updates allocation descriptor"/imm32
    68/push  0xb/imm32
    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy *ecx to eax
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           3/r32/ebx   .               .                 # subtract ebx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # clean up
    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32  1/imm32     # copy to *Next-alloc-id
    # . reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x20/imm32        # add to esp
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

copy-array:  # ad: (addr allocation-descriptor), src: (addr array _T), out: (addr handle array _T)
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = src
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   0xc/disp8       .                 # copy *(ebp+12) to esi
    # var size/ecx: int = src->size+4
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           1/r32/ecx   .               .                 # copy *esi to ecx
    81          0/subop/add         3/mod/direct    1/rm32/ecx    .           .             .           .           .               4/imm32           # add to ecx
    # allocate(ad, size, out)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x10/disp8      .                 # push *(ebp+16)
    51/push-ecx
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
    # . . call
    e8/call  allocate/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # var payload/eax: (addr byte) = out->payload
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0x10/disp8      .                 # copy *(ebp+16) to eax
    8b/copy                         1/mod/*+disp8   0/rm32/eax    .           .             .           0/r32/eax   4/disp8         .                 # copy *(eax+4) to eax
    # . skip payload->allocid
    05/add-to-eax  4/imm32
    # var max/ecx: (addr byte) = payload + size
    01/add                          3/mod/direct    1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # add eax to ecx
    # _append-4(payload, max, src, &src->data[src->size])
    # . . push &src->data[src->size]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/esi  2/index/edx   .           2/r32/edx   4/disp8         .                 # copy esi+edx+4 to edx
    52/push-edx
    # . . push src
    56/push-esi
    # . . push max
    51/push-ecx
    # . . push payload
    50/push-eax
    # . . call
    e8/call  _append-4/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
$copy-array:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

test-copy-array:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var src/esi: (addr array int) = [3, 4, 5]
    68/push  5/imm32
    68/push  4/imm32
    68/push  3/imm32
    68/push  0xc/imm32/size
    89/copy                         3/mod/direct    6/rm32/esi    .           .             .           4/r32/esp   .               .                 # copy esp to esi
    # var ad/ecx: allocation-descriptor containing 32 bytes
    # . var end/ecx: (addr byte)
    89/<- %ecx 4/r32/esp
    # . var start/edx: (addr byte) = end - 32
    81 5/subop/subtract %esp 0x20/imm32
    89/<- %edx 4/r32/esp
    # . ad = {start, end}
    51/push-ecx
    52/push-edx
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var expected-payload/ebx = ad->curr
    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           3/r32/ebx   .               .                 # copy *ecx to ebx
    # var h/edx: handle = {0, 0}
    68/push  0/imm32
    68/push  0/imm32
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # *Next-alloc-id = 0x34
    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32  0x34/imm32  # copy to *Next-alloc-id
    # copy-array(ad, src, h)
    # . . push args
    52/push-edx
    56/push-esi
    51/push-ecx
    # . . call
    e8/call  copy-array/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->alloc-id, 0x34, msg)
    # . . push args
    68/push  "F - test-copy-array: sets alloc-id in handle"/imm32
    68/push  0x34/imm32
    ff          6/subop/push        0/mod/indirect  2/rm32/edx    .           .             .           .           .               .                 # push *edx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->payload, expected-payload, msg)
    # . . push args
    68/push  "F - test-copy-array: sets payload in handle"/imm32
    53/push-ebx
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(h->payload->alloc-id, 0x34, msg)
    # . . push args
    68/push  "F - test-copy-array: sets alloc-id in payload"/imm32
    68/push  0x34/imm32
    ff          6/subop/push        0/mod/indirect  2/rm32/edx    .           .             .           .           .               .                 # push *edx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # var payload/eax: (addr int) = lookup(h)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    ff          6/subop/push        0/mod/indirect  2/rm32/edx    .           .             .           .           .               .                 # push *edx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-ints-equal(payload->size, 0xc, msg)
    # . . push args
    68/push  "F - test-copy-array: sets array size in payload"/imm32
    68/push  0xc/imm32
    ff          6/subop/push        0/mod/indirect  0/rm32/eax    .           .             .           .           .               .                 # push *eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(*Next-alloc-id, 0x35, msg)
    # . . push args
    68/push  "F - test-copy-array: increments Next-alloc-id"/imm32
    68/push  0x35/imm32
    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32              # push *Next-alloc-id
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(ad->curr - expected-payload, 12 + 4 for alloc-id + 4 for size, msg)
    # . . push args
    68/push  "F - test-copy-array: updates allocation descriptor"/imm32
    68/push  0x14/imm32
    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy *ecx to eax
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           3/r32/ebx   .               .                 # subtract ebx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # clean up
    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Next-alloc-id/disp32  1/imm32     # copy to *Next-alloc-id
    # . reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x40/imm32        # add to esp
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# Fill a region of memory with zeroes.
zero-out:  # start: (addr byte), size: int
    # pseudocode:
    #   curr/esi = start
    #   i/ecx = 0
    #   while true
    #     if (i >= size) break
    #     *curr = 0
    #     ++curr
    #     ++i
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    56/push-esi
    # curr/esi = start
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var i/ecx: int = 0
    31/xor                          3/mod/direct    1/rm32/ecx    .           .             .           1/r32/ecx   .               .                 # clear ecx
    # edx = size
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           2/r32/edx   0xc/disp8       .                 # copy *(ebp+12) to edx
$zero-out:loop:
    # if (i >= size) break
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    7d/jump-if->=  $zero-out:end/disp8
    # *curr = 0
    c6          0/subop/copy-byte   0/mod/direct    6/rm32/esi    .           .             .           .           .               0/imm8            # copy byte to *esi
    # ++curr
    46/increment-esi
    # ++i
    41/increment-ecx
    eb/jump  $zero-out:loop/disp8
$zero-out:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

test-zero-out:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # region/ecx = 34, 35, 36, 37
    68/push  0x37363534/imm32
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # zero-out(ecx, 3)
    # . . push args
    68/push  3/imm32/size
    51/push-ecx
    # . . call
    e8/call  zero-out/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # first 3 bytes cleared, fourth left alone
    # . check-ints-equal(*ecx, 0x37000000, msg)
    # . . push args
    68/push  "F - test-zero-out"/imm32
    68/push  0x37000000/imm32
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # . reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# . . vim:nowrap:textwidth=0