Each time you compile a source code to an object file is assigned a "virtual address" (a "relative" address) to each memory reference in your code. Example:
bits 32
section .data
x: dd 0
section .text
global f
f:
inc dword [x]
ret
If you compile this you'll get:
$ nasm -l test.lst test.asm
$ cat test.lst
1 bits 32
2
3 section .data
4
5 00000000 00000000 x: dd 0
6
7 section .text
8
9 global f
10 f:
11 00000000 FF05[00000000] inc dword [x]
12 00000006 C3 ret
Notice `x` get the offset 0 in `.data`section. and `f` got offset 0 on `.text` section. (and the offset in inc instruction is [00000000]).
When the linker is used with multiple modules it atributes different offsets to these references... Let's say you have another module (test2.asm) defining `y` as DWORD... to that object file `y` will get the offset 0 as well, but the linker puts `x` and `y` in the same section, assignining a different offsets for these 2 symbols.. The same to function's entrypoints...
Notice, also, that CALL/JMP and conditional jumps use relative addressing (relative to EIP ou RIP)...
In the case of DLLs, when they are loaded, the offset is avaliable in PE file format. In Windows, when you use GetProcAddress you get this address and assign to a function pointer to do an indirect call (late binding). Or the linker do this for you (early binding).
As for DLLs with functions with the same name, in early binding it can be problematic, but with late biding it has no problem, since the same is used only to find the address where the function is...