Most operating systems support a concept called "sections" in their executable file format (possibly using a different name). These sections can be given different attributes (like, read-only, read/write, executable, etc.) and can be loaded into different parts of an application's address space. For example, the section with the code could go to the lower part of the address space, while the globals are placed in the upper part (with everything in between available for stack and heap).
With dynamically loaded libraries, the same principles can be used to have a section with code that gets shared between the different applications, and a section with variables that gets loaded separately for each application.
One additional feature that is needed to make this work is the use of virtual addresses. With virtual addresses, there is no fixed correspondence between the addresses as seen by an application and the cells of physical memory. This allows the OS to swap pages of memory in and out of the RAM, but it also provides the means to share pages of physical memory between processes (even if they don't correspond to identical virtual addresses in those processes) and to have identical virtual addresses in two processes refer to different physical locations.
Thus, most of the work in making dynamically loaded libraries work is done by the OS. The compiler and linker just have to put everything in the appropriate sections, so the unmodifiable stuff can be shared between processes, while the variables get allocated separately for each process.