Since I was still learning the x86 CPUs, the intention here was not to save bytes but instead to have something working. I believe I picked up the style of having the string at the top and jumping over it from other similar code I had come across in those days.
You are right of course. Here is a complete example that moves the string to the bottom and saves two bytes:
C:\>DEBUG
-A
1165:0100 MOV AH, 9
1165:0102 MOV DX, 10B
1165:0105 INT 21
1165:0107 MOV AH, 0
1165:0109 INT 21
1165:010B DB 'hello, world', D, A, '$'
1165:011A
-G
hello, world
Program terminated normally
-N HELLO.COM
-R CX
CX 0000
:1A
-W
Writing 0001A bytes
-Q
C:\>HELLO
hello, world
C:\>
I have now updated the GitHub repository with this updated source code and binary. Thank you for the nice comment!Maybe someone here knows what was that purpose?
I was surprised when he told me how people normally passed arguments. They would put them after the JMS instruction in the caller. So, the callee would indirect from the "return address" stored in FOO to retrieve its arguments, incrementing the return address after each one. So the code to call FOO with arguments 1 and 2 would be (in Intel syntax)
JMS FOO
DW 1
DW 2
[code to run after returning]
Mark Smotherman explains this in more detail, using PDP-8-compatible syntax, in https://people.computing.clemson.edu/~mark/subroutines/pdp8.....It seems sort of intuitive to do things this way; if you're writing atan2(3, 4), it's nice to be able to translate this to
jms atan2
3
4
rather than, say, mov $3, %rdi; mov $4, %rsi; call atan2.Maybe someone had this habit in their assembly and adapted it to the 8086's calling convention by inserting MOVs and a JMP instruction?
You can also imagine that a compiler might be somewhat simpler if it generated code in the order in which it encountered the expressions. Gas lets you switch back and forth between .text and .data, so that if your compiler does this, you still don't have to insert JMP instructions. However, a few years ago when I wrote Ur-Scheme, I did do this for lambda expressions: the code for the lambda expression is generated inline in the middle of its containing code, which then has to contain a jump past it.
mov ah, 9
mov dx, offset helloworld
int 21
mov ah, 0
int 21
.helloworld:
db 'hello, world', d, A, '$'Hence you declare your variables at the start of the program and jump past them.
Minimizing the number of forward references may make the assembly run faster or use less memory, but usually it's just a convention to make the source code more neatly organized.
MOV AH, 9
MOV DX, str
INT 21
MOV AH, 0
INT 21
Str:
DB 'hello, world', d, A, '$'
MOV AH, 9
MOV DX, str
INT 21
RET
str:
DB 'hello, world', d, A, '$'
Why can MOV AH, 0
INT 21
be replaced by RET? Here is the answer: https://stackoverflow.com/a/60805758UPDATE: Under https://news.ycombinator.com/item?id=33398592 userbinator posted an additional possible optimization.
INT 21
RET
can be replaced with JP 5I don't see why this would be faster than using RET directly and having the INT 20 opcode at address 0 do the work.