The apparent "off by one" is explained by the fact that "$" refers to the address at the *beginning* of the line. Assemble "mov eax, $" - just that - and you can see what's happening.
A label - including "$" - is a "relocatable value" (as opposed to what Nasm calls a "scalar" value... in the error message only...). The reason why "loop" requires a label and "jcc" doesn't is... damned if I know, they're both "relative addresses", I think... Unless you want to delve deep into the nitty-gritty, accept that "that's how Nasm does it".
If you care to say... why don't you want to use labels in this "special case"?
Best,
Frank