CPUs have gotten wider, and much faster, but they still haven't implemented the "do what I meant" instruction.
Your primary problem is more of a "C problem" than an "asm problem", really. You've essentially said printf("%d");... but haven't given it an integer to print! Printf looks at your "format string", and sees it expects one more parameter - an integer to be displayed as decimal - so it prints whatever is next up the stack... probably the return address for "main".
So you want to push the number to print first, and then the format string saying to print it. You'll want to remove both parameters after the call - "add esp, 8" (I like to write it as "add esp, 4 * 2", to indicate 2 parameters at 4 bytes each - not important, but it needs to be
. You've got the number to print in ecx (so "push ecx"), and the "ending value" in eax. Good plan, but unfortunately it turns out that these are both bad choices...
The "C calling convention", besides specifying that parameters are pushed on the stack right to left, specifies that certain registers must retain their values across calls, other registers are "scratch" and can be trashed, the return value (if any - "printf" returns the number of items printed, I think) is in eax. Oops. The registers which must be preserved are ebx, esi, edi, and ebp (esp needs to be right, but for a different reason - "ret" depends on it). This leaves ecx (and edx, unless the function returns a 64-bit value) as "scratch". Oops again. No guarantee that "printf" will alter ecx "behind our back", but it's allowed to... and I think you'll find that it does.
We can still use all the registers, we just need to be aware of the "rules". You could continue to use eax as an "ending value" and ecx as the number to print...
push eax ; save regs printf will trash
push ecx
push ecx ; number to print
push counter ; format string
call printf ; oops, "_printf" for 'doze
add esp, 4 * 2 ; "clean up" stack - remove parameters
pop ecx ; restore registers we were using
pop eax
...
You will observe that we've pushed ecx twice, and if we were "clever" we could have utilized the same push to pass the parameter and preserve the value that printf is (likely) going to trash. Such "cleverness" would make the code difficult to "maintain" - I'd avoid it. You could use some of the registers that printf *will* preserve - if you do that, remember that main's caller (C's "startup" code - crt0.o or so) expects 'em to be preserved, and we have to do it if we alter 'em! You could also use "local variables" on the stack - overkill in this case (but might make a nice example).
I don't know why you're doing that first call to printf, but at the end of your "loop" (since "loop" is the name of an instruction, it's a terrible name for a label, IMHO - name it for what it does - "number_printing_loop:" or something - but it'll work - or would, if you actually jumped to it) you've got:
cmp ecx, eax
jl finished
finished:
; good from here
It's confusing, since Intel syntax has the operands in "opposite" order, but this jumps if ecx is less than eax, not if eax is less than ecx (still confuses me, once in a while!). So you probably want to jump to "number_printing_loop" (or "loop") - actually you'd want to jump if less or equal. Since eax and ecx were being trashed behind your back, I'm not surprised you had trouble getting this right. Oh, and if the jump isn't taken, what happens?
Strictly speaking, "jl" (and "jg") apply to signed numbers. Anything with the high bit set is considered negative. Since a "count" is logically unsigned, you really should be using "jb" (jump if first operand is "below" second operand) and/or "ja" (jump if "above"). It won't cause a problem here - you'll get bored long before you've printed two billion and change numbers (7FFFFFFFh - don't expect me to remember all those digits in decimal! - is the "biggest" number jl/jg would consider "greater" - after that it's negative)
A compiler wants to know if a number is signed or unsigned, and will whine if you attempt to compare signed with unsigned. It needs to know whether to emit "jg" or "ja"! Since we're selecting the instructions here, we need to know (Nasm doesn't care - neither is an error). The condition codes seem so confusing - there are so many of them! Once you realize that a good many of them are aliases for the same opcode (je=jz, jb=jc" jnb=jge=jnc, etc...) it becomes a little more manageable. But there *is* a difference with signed vs unsigned condition codes - the former check the overflow flag (xor's it with the carry flag, typically). You don't need to grasp this all at once - learn a couple simple ones, and work outwards from there.
Anyway...
Give printf the correct number of parameters, be aware of which registers printf can alter, and straighten out your "loop control" a little... I think you can fix it fairly easily.
Best,
Frank