1 I left myself a nice easy one to start this log:
2
3 [x] Make all words take params from the stack, not
4 from pre-defined registers.
5
6 Which ought to be simple: just push the values
7 I need before calling the function. Then have
8 the function pop the values into the registers
9 and off we go.
10
11 +----------------------------------------------------+
12 | NOTE: I'm still using call/ret to use the |
13 | 'find' and 'inline' words when the program |
14 | initially runs. I've got a bit of a |
15 | chicken-and-egg problem here because without a |
16 | return, these won't seamlessly move on to the |
17 | next instructions when they're done and I |
18 | can't find or inline them because THEY are |
19 | 'find' and 'inline'! |
20 | |
21 | I feel like that will be solved when I've got |
22 | more of the interpreter or REPL in place. If |
23 | not, I've got a puzzle on my hands. For the |
24 | moment, things are just a bit...messy. |
25 +----------------------------------------------------+
26
27 Well, I thought that was going to be easy. I mean, it is
28 pretty easy. But it has a few snags I hadn't yet
29 considered.
30
31 It turns out, using the same stack for call/ret return
32 address storage AND for passing values between functions
33 in a truly concatenative manner gets real complicated
34 real quick. And since I was using call/ret temporarily
35 anyway, I have zero desire to do anything fancy to make
36 it work.
37
38 So I'm going to basically do my own return by storing a
39 return address and jumping to it at the end of both
40 'inline' and 'find'.
41
42 I'm making a new variable in BSS to hold my return
43 address. I only need one, not a stack, because I'm not
44 making any nested calls.
45
46 temp_return_addr: resb 4
47
48 I'll put in a mockup of the code to get the assembled
49 instruction lengths right (I hope) so I can figure out
50 the address we just jump back to as a "return". (I'm
51 pretty sure I can't just store the instruction pointer
52 register because that'll be a point before the "call"
53 jump and then I'll have an infinite loop, right?)
54
55 I'll use NASM's listing feature for that. It comes out
56 super wide (well, compared to the 60 columns I give
57 myself on my little split screen setup!), so I'll see if
58 I can reformat it enough to fit here:
59
60 152 00DA 68[0600] push temp_meow_name
61 153 00DD 66C706[0904]- mov dword [temp_return_addr], $
62 153 00E2 [DD000000]
63 154 00E6 EB8A jmp find
64 155 00E8 6650 push eax
65
66 The listing is so fun to look at and I find it almost
67 fetishisticly beautiful. I mean, I've had all of these
68 _questions_ about how all of this actually works and
69 here, if you can read them, are all of the _answers_. I
70 mean, I know the CPU still has secrets down below even
71 this machine code layer. But for the application
72 programmer, this is _it_. This is the bedrock upon which
73 we lay all of our hopes and dreams. In the hex column on
74 the left are the real instructions, no longer hidden by
75 mnemonics or symbols.
76
77 Anyway, where was I?
78
79 Oh, yeah, so '$' is NASM for "the address at the
80 beginning of this line". Which is very handy. And that's
81 exactly what's gonna be put into temp_return_addr:
82
83 0904 is little-endian for temp_return_addr at 0409
84 (which I could see further up my listing)
85
86 DD000000 is the address returned by $
87 (again, in little-endian)
88
89 And assuming the assembled code won't change, it looks
90 like I want my return address to be an additional...
91
92 E8 - DD
93
94 ...bytes. Which, uh, I'll ask my cat to subtract for me.
95
96 Hmm. No, he purred, but was not forthcoming with the
97 answer. Okay, how about dc?
98
99 $ dc
100 16 i
101 e8 dd - p
102 dc: 'e' (0145) unimplemented
103 E8 DD - p
104 11
105
106 Okay, so dc hissed at me once for not entering the hex
107 values in upper case. So score one point for my cat. But
108 then it gave me the correct answer after that, so score
109 one point for dc. Looks like this match is even.
110
111 So I wanna add 11 bytes to my return addresses.
112
113 Here's the new listing:
114
115 153 00DD 66C706[0904]- mov dword [temp_return_addr], ($ + 11)
116 153 00E2 [E8000000]
117 154 00E6 EB8A jmp find
118 155 00E8 6650 push eax
119
120 Looks right to me, we want to jump ("ret") back to 00E8
121 after the jump ("call") to find.
122
123 Of course, this seems super fragile, but it's also super
124 temporary. Let's just see if it works...
125
126 Okay, dang it, a segfault. My changes have required
127 another change and now the addresses are a little
128 different, but the 11 bytes should still be the same:
129
130 154 000000DF 66C706[0904]- mov dword [temp_return_addr], ($ + 11)
131 154 000000E4 [EA000000]
132 155 000000E8 EB88 jmp find
133 156 000000EA 6650 push eax
134
135 Let's try it now:
136
137 (gdb) break find.found_it
138 Breakpoint 1, find.found_it () at meow5.asm:116
139 116 mov eax, edx ; pointer to tail of dictionary word
140 (gdb) p/a $edx
141 $1 = 0x8049030 <meow_tail>
142
143 So far so good. Now the return jump?
144
145 (gdb) s
146 117 jmp [temp_return_addr]
147 (gdb) p/a (int)temp_return_addr
148 $2 = 0x80490df <inline_a_meow+16>
149
150 And just where might that be, exactly?
151
152
153 0x080490d4 <+5>: c7 05 19 a4 04 08 df 90 04 08 movl
154 $0x80490df, 0x804a419
155 0x080490de <+15>: eb 88 jmp 0x8049068 <find>
156 0x080490e0 <+17>: 50 push %eax
157 0x080490e1 <+18>: e8 57 ff ff ff call 0x804903d <inline>
158
159 Hmmm...looks off by 1. 0x80490df points to the second
160 byte of the jmp find instruction...
161
162 Program received signal SIGSEGV, Segmentation fault.
163 0x080490df in inline_a_meow () at meow5.asm:155
164 155 jmp find ; answer will be in eax
165
166 Yeah. So... 12 bytes?
167
168 And to think, I waxed all poetic about the NASM listing.
169 I don't know how to explain the byte discrepancy. Let's
170 see if this works:
171
172 (gdb) break find.found_it
173 Breakpoint 1, find.found_it () at meow5.asm:116
174 116 mov eax, edx ; pointer to tail of dictionary word
175 (gdb) s
176 117 jmp [temp_return_addr]
177 (gdb) s
178 inline_a_meow () at meow5.asm:156
179 156 push eax ; put it on the stack for inline
180
181 Yes! But that was evidently even _more_ fragile than I'd
182 expected. So I'll just bit the bullet and hold my nose
183 and use some temporary labels. It's still quite
184 compact, so I'll just paste it here:
185
186 push temp_meow_name ; the name string to find
187 mov dword [temp_return_addr], t1
188 jmp find ; answer will be in eax
189 t1: push eax ; put it on the stack for inline
190 mov dword [temp_return_addr], t2
191 jmp inline
192 t2: dec byte [meow_counter]
193 jnz inline_a_meow
194
195 ; inline exit
196 push temp_exit_name ; the name string to find
197 mov dword [temp_return_addr], t3
198 jmp find ; answer will be in eax
199 t3: push eax ; put it on the stack for inline
200 mov dword [temp_return_addr], t4
201 jmp inline
202 t4:
203 ; Run!
204 push 0 ; push exit code to stack for exit
205 jmp data_segment ; jump to the "compiled" program
206
207 Does it work?
208
209 dave@cygnus~/meow5$ mr
210 Meow.
211 Meow.
212 Meow.
213 Meow.
214 Meow.
215
216 Yes!
217
218 One last thing, now - 'find' is still leaving its answer
219 in the eax register. If I have it push the answer to the
220 stack instead, 'inline' will pop it and have what it
221 needs - no need for that "push eax" beween the two
222 functions/words (at labels t1 and t3 above).
223
224 Now find.not_found and find.found_it push their return
225 values on the stack:
226
227 .not_found:
228 push 0 ; return 0 to indicate not found
229 jmp [temp_return_addr]
230
231 .found_it:
232 push edx ; return pointer to tail of dictionary word
233 jmp [temp_return_addr]
234
235 And the calls simply flow one after the other without
236 any explicit data passing:
237
238 jmp find
239 t1: mov dword [temp_return_addr], t2
240 jmp inline
241 t2: ...
242
243 And does _that_ work?
244
245 dave@cygnus~/meow5$ mr
246 Meow.
247 Meow.
248 Meow.
249 Meow.
250 Meow.
251
252 Yes, and now I can check that little box at the top of
253 this log. We're doing pure stack-based concatenative
254 programming now.
255
256 Next step:
257
258 [x] Parse the string "meow meow meow meow meow exit"
259 as a program (pretend we're already in "compile
260 mode" and we're gathering word tokens and
261 compiling them) and execute it.
262
263 It begins! Here's the string in the .data segment:
264
265 input_buffer_start:
266 db 'meow meow meow meow meow exit', 0
267 input_buffer_end:
268
269 And here's the .bss segment "variables":
270
271 token_buffer: resb 32 ; For get_token
272 input_buffer_pos: resb 4 ; Save position of read tokens
273
274 Yup, just 32 chars for token names (well, 31 because I'm
275 null-terminating the string). Hey, it's my language. Ha
276 ha, I can always bump this up later. But 31 is actually
277 quite long, you know?
278
279 abcdefghijklmnopqrstuvwxyz01234
280
281 I've created a word called 'get_token' which will do the
282 job of both 'WORD' and 'KEY' in Forth. And I was just
283 about to 'call' it to test it, but I can't bear to put
284 in another manual temporary label
285
286 So, it's macro time!
287
288 %macro CALLWORD 1
289 mov dword [return_addr], %%return_to
290 jmp %1
291 %%return_to:
292 %endmacro
293
294 And it should be super easy to use. First, I'll test my
295 temporary "manual" 'meow' and 'exit' inlines to make
296 sure it works. They're about to go away, but they'll
297 make a good test.
298
299 Look at how clean the 'exit' one is:
300
301 push temp_exit_name ; the name string to find
302 CALLWORD find
303 CALLWORD inline
304
305 But does it work?
306
307 dave@cygnus~/meow5$ mr
308 Meow.
309 Meow.
310 Meow.
311 Meow.
312 Meow.
313
314 First try! No way. I mean, of _course_ it worked first
315 try and I never doubted it would.
316
317 Okay, now let's get into this get_token function:
318
319 (gdb) break get_next_token
320 Breakpoint 1 at 0x804912e: file meow5.asm, line 189.
321 (gdb) r
322 Starting program: /home/dave/meow5/meow5
323
324 Breakpoint 1, get_next_token () at meow5.asm:189
325 189 mov dword [return_addr], %%return_to ; CALLWORD
326 190 jmp %1 ; CALLWORD
327 150 mov ebx, [input_buffer_pos] ; set input read addr
328 151 mov edx, token_buffer ; set output write addr
329 152 mov ecx, 0 ; position index
330 154 mov al, [ebx + ecx] ; input addr + position index
331 155 cmp al, 0 ; end of input?
332 (gdb) p/c $al
333 $2 = 109 'm'
334
335 Nice! So the 'm' from the first 'meow' has been
336 collected so far. Now the rest of the token...
337
338 (gdb) break 155
339 Breakpoint 2 at 0x80490c7: file meow5.asm, line 155.
340 (gdb) c
341 155 cmp al, 0 ; end of input?
342 (gdb) p/c $al
343 $3 = 101 'e'
344 ...
345 $4 = 111 'o'
346 $5 = 119 'w'
347 $6 = 32 ' '
348
349 We have 'meow' and the space should be our token
350 separator.
351
352 155 cmp al, 0 ; end of input?
353 156 je .end_of_input ; yes
354 157 cmp al, ' ' ; token separator? (space)
355 158 je .return_token ; yes
356 170 add [input_buffer_pos], ecx ; save input position
357 171 mov [edx + ecx], byte 0 ; terminate str null
358
359 Looks good. Did we collect 4 characters as expected?
360
361 (gdb) p $ecx
362 $7 = 4
363
364 Yup. Then get_token will "return" the token string
365 address so 'find' can use it to find the 'meow' word:
366
367 172 push DWORD token_buffer ; return str address
368 173 jmp [return_addr]
369 219 cmp DWORD [esp], 0 ; check return without popping
370 220 je run_it ; all out of tokens!
371 189 mov dword [return_addr], %%return_to ; CALLWORD
372 190 jmp %1 ; CALLWORD
373 96 pop ebp ; first param from stack!
374 find () at meow5.asm:99
375 99 mov edx, [last]
376
377 Okay, the execution looks right. And did we pass the
378 address correctly on the stack?
379
380 (gdb) p $ebp
381 $9 = (void *) 0x804a43d <token_buffer>
382
383 Yup! And does it contain the expected 'meow' token?
384
385 (gdb) x/s $ebp
386 0x804a43d <token_buffer>: "meow"
387
388 Nice!
389
390 I'm going to assume 'find' and 'inline' will work
391 correctly. Let's see if we can get the next token from
392 the input string:
393
394 (gdb) c
395 Continuing.
396 Breakpoint 2, get_token.get_char () at meow5.asm:155
397 155 cmp al, 0 ; end of input?
398
399 Alright, we're back in get_token. This should be the
400 first character of the second 'meow' token:
401
402 (gdb) p/c $al
403 $10 = 32 ' '
404
405 Uh oh. That doesn't look right. I'll continue anyway...
406
407 (gdb) c
408 Continuing.
409
410 Program received signal SIGSEGV, Segmentation fault.
411 inline () at meow5.asm:76
412 76 mov ecx, [esi + 4] ; get len into ecx
413
414 Yeah, that makes sense. 'find' will have
415 failed to find the '' token and then 'inline' crashes when
416 trying to read from address 0 (the null pointer return
417 value from 'find').
418
419 The best way to handle this is probably to ignore any
420 leading spaces - that will not only be useful later, it
421 will take care of this current character problem.
422
423 In a higher-level language, I might choose to do this
424 with nested logic, like so:
425
426 if (char === ' ')
427 if (token.len > 0)
428 'eat' space (move to next input char)
429 else
430 return the token
431 end
432 end
433
434 But in assembly, this all gets flattened. It's a
435 surprisingly interesting exercise to formulate the logic
436 in terms of jumps. (At least at first. I'm sure the
437 novelty wears off after a while.)
438
439 Anyway, here's my solution:
440
441 cmp al, ' ' ; token separator? (space)
442 jne .add_char ; nope! get char
443 cmp ecx, 0 ; yup! do we have a token yet?
444 je .eat_space ; no
445 jmp .return_token ; yes, return it
446 .eat_space:
447 inc ebx ; 'eat' space by advancing input
448 jmp .get_char
449
450 I'll make sure that works in GDB. I changed the input
451 string to:
452
453 db ' meow meow meow meow meow exit', 0
454
455 with a leading space and two spaces before the second
456 meow token. That'll make it easy to test:
457
458 155 cmp al, 0 ; end of input?
459 (gdb) p/c $al
460 $1 = 32 ' '
461 157 cmp al, ' ' ; token separator? (space)
462 158 jne .add_char ; nope! get char
463 162 cmp ecx, 0
464 163 je .eat_space
465 171 inc ebx
466 172 jmp .get_char
467
468 Yup! That line 171 is my "eat the leading space" action
469 and now we should get the 'm' in "meow" and store it:
470
471 154 mov al, [ebx + ecx] ; input addr + position index
472 155 cmp al, 0 ; end of input?
473 156 je .end_of_input ; yes
474 (gdb) p/c $al
475 $2 = 109 'm'
476 157 cmp al, ' ' ; token separator? (space)
477 158 jne .add_char ; nope! get char
478 175 mov [edx + ecx], al ; write character
479
480 Yeah. This is looking good.
481
482 You know what? I'm just gonna go for it:
483
484 dave@cygnus~/meow5$ mr
485 Meow.
486 Meow.
487 Meow.
488 Meow.
489 Meow.
490
491 Yes! So that's another item checked off!
492
493 This is really coming along.
494
495 At nearly 500 lines, this log is complete. I'll see you
496 in the next one, log04.txt. :-)