1) In a language like C or C++, you wouldn't do something like this:
foo (1,2);
x = bar (2.5);
It makes it harder to read the code because the function name makes no sense without its parameters. The same is true for assembly. Basically, this:
xor EAX, EAX
Should be this:
xor EAX, EAX
2) In a language like C or C++, you wouldn't do something like this:
b = a * 5 + 2298;
c = b / 9;
Instead, you might do this:
kelvin_x9 = fahrenheit * 5 + 2298;
kelvin = kelvin_x9 / 9;
In assembly, because you can't give registers descriptive names (in the same way that you can give variables descriptive names in higher level languages) and because often you do things that are semantically wrong (e.g. using shifts when you mean multiplication, using LEA when you're not loading an effective address, etc) it's extremely important for code maintainability to use comments. Basically this:
calc_lcm:
mov EAX, [num1_cpy]
mov EDX, [num2_cpy]
mul EDX
mov EDI, EAX
xor EBX, EBX
mov EDX, EDI
shr EDX, 16
mov EAX, EDI
mov BX, [gcd]
div BX
mov SI, AX
mov [lcm], SI
ret
Should be this:
calc_lcm:
mov EAX, [num1_cpy] ;eax = number1
mov EDX, [num2_cpy] ;edx = number2
mul EDX ;edx:eax = number1 * number2
mov EDI, EAX ;edi = number1 * number2, high 32-bits of number discarded
xor EBX, EBX
mov EDX, EDI ;edx = number1 * number2
shr EDX, 16 ;edx = number1 * number2 / 65536, dx:ax = number1 * number2
mov EAX, EDI ;eax = number1 * number2, (can be deleted as EAX already contained this value)
mov BX, [gcd] ;bx = GCD
div BX ;ax = number1 * number2 / GCD, dx = number1 * number2 % GCD
mov SI, AX ;si = number1 * number2 / GCD
mov [lcm], SI
ret
For assembly there's only 2 types of bugs - comments that don't describe sane logic, and code that doesn't do what the comments say it should. This allows you to debug the code effectively and drastically reduces the chance of missing mistakes (in addition to the hopefully obvious massive improvement in code readability).
3) In a language like C or C++, you would create lots of bugs caused by things like integer overflows because the language doesn't provide a sane/easy way to detect these bugs. In assembly you should not create lots of bugs caused by things like integer overflows because you can easily test for overflows.
For example, this:
calc_lcm:
mov EAX, [num1_cpy] ;eax = number1
mov EDX, [num2_cpy] ;edx = number2
mul EDX ;edx:eax = number1 * number2
mov EDI, EAX ;edi = number1 * number2, high 32-bits of number discarded
Should be this:
calc_lcm:
mov EAX, [num1_cpy] ;eax = number1
mov EDX, [num2_cpy] ;edx = number2
mul EDX ;edx:eax = number1 * number2
test EDX, EDX ;Will the result fit in 32 bits?
jne .overflow ; no, error
mov EDI, EAX ;edi = number1 * number2
Note: A better idea would be to do "movzx ebx,word [GCD]" and "div ebx" to divide EDX:EAX by the GCD instead of pointlessly converting the 64-bit value you had in EDX:EAX into a 32-bit value in DX:AX.
4) A compiler will check if (e.g.) functions are being called with the correct number of parameters, and will make sure that various registers are saved/restored, etc where necessary. In assembly there is none of that; so you have to do it manually, so you have to be able to do it manually. To be able to do it manually you need to document routines properly. For example, this:
calc_lcm:
Should be this:
;Calculate Lowest Common Multiple
;
;Input:
; [num1_cpy] Some number
; [num2_cpy] Some other number
; [GCD] The GCD
;
;Output:
; [lcm] The result
;
;Trashed:
; eax, edx, ebx, esi, edi
calc_lcm:
Normally, you'd pass parameters in registers and return parameters in registers to avoid wasting time on loading/storing where avoidable. For example, the entire "calc_lcm" code might be:
;Calculate Lowest Common Multiple
;
;Input:
; eax Some number
; edx Some other number
; ebx The GCD
;
;Output:
; eax The result
;
;Trashed:
; edx
calc_lcm:
mul EDX ;edx:eax = number1 * number2
div EBX ;eax = (number1 * number2) / GCD, edx = (number1 * number2) % GCD
ret
5) If you're writing assembly you should write assembly; and not attempt to obfuscate the code by inventing your own language that makes it difficult read the code and impossible to effectively optimise the code.
For example, this code:
nwln
nwln
PutStr num_lbl
nwln
nwln
GetLInt [num1]
GetLInt [num2]
Is 100% meaningless gibberish.
I don't know what this code should be (and am completely unable to see ways of optimising it that would have been obvious if it was assembly) because I have no idea what any of these macros do. I can't tell which registers get trashed, I can't see which pushes/pops/moves are redundant. I might be able to assume that it'd be safe to delete the first 2 nwln
lines and change the string to num_lbl: DB "\n\n> Numbers (2): ", 0
but then that could introduce all sorts of bugs because I didn't bother with the hassle of finding and reading the macros that may do more (or less) than whatever I've assumed they might do.