I think you're confusing how executing CIL works with how executing native code works.
With CIL, the code that is in the executable file is not actually executed. Usually (with the desktop CLR and without ngen) the CLR reads the CIL and generates actual executable code on the fly (that's why this part of the CLR is called “just in time compiler”).
CIL opcodes like call
use tokens to reference methods and other members. In the generated native code, those are translated into the address of the native code for that method.
Opcodes like br
contain offsets relative to the next CIL instruction and they can jump only inside the current method. On x86, they are compiled to instructions like jne
, which contain offsets relative to the next x86 instruction.
But the metadata for a method is described in the MethodDef
table, which contains reference to the IL stream for that method as a 4-byte Relative Virtual Address (and RVAs are used in other parts of the file too). I think this means that the executable file can't actually be more than 4 GB in size.
I attempted to verify this by creating a large executable using Reflection.Emit. But the best I could do on my system with 8 GB of RAM was crating a 1.5 GB big file (which already made the computer unusable due to swapping).