ia64 – misdeclaring near and far data
As I mentioned yesterday,
the ia64 is a very demanding architecture.
Today I’ll discuss another way that lying to the compiler will
come back and bite you.
The ia64 does not have an absolute addressing mode.
Instead, you access your global variables through the r1
register, nicknamed “gp” (global pointer).
This register always points to your global variables.
For example, if you had three global variables, one
of them might be kept at [gp+0], the second at [gp+8]
and the third at [gp+16].
(I believe the Win32 MIPS calling convention also used this technique.)
On the ia64,
there is a limitation in the “addl” instruction: You can only
add constants up to 22 bits, which comes out to 4MB.
So you can have only 4MB of global variables.
Well, it turns out that some people want more than 4MB of
global variables. Fortunately, these people don’t have
one million DWORD variables. Rather, they have a few
really big global arrays.
The ia64 compiler solves this problem by splitting global
variables into two categories, “small” and “large”.
(The boundary between “small” and “large” can be set by
a compiler flag. I believe the default is to treat anything
larger than 8 bytes as “large”.)
The code to access a “small” variable goes like this:
addl r30 = -205584, gp;; // r30 -> global variable ld4 r30 = [r30] // load a DWORD from the global variable
(The gp register actually points into the middle of your global
variables, so that both positive and negative offsets can be used.
In this case, the variable happened to live at a negative offset
By comparison, “large” global variables are accessed through a
two-step process. First, the variable itself is allocated in a
separate section of the file. Second, a pointer to the variable
is placed into the “small” globals variables section of the
module. As a result, accessing a “large” global variable requires
an added level of indirection.
addl r30 = -205584, gp;; // r30 -> global variable forwarder ld8 r30 = [r30];; // r30 -> global variable ld4 r30 = [r30] // load a DWORD from the global variable
If you leave the size of an object unspecified, like
extern BYTE b;
then the compiler plays it safe and assumes the variable is large.
If it turns out that the variable is small,
the forwarder pointer will still be there, and the code will
do the double-indirection to fetch something that could have
been accessed with a single indirection.
The code is slightly less efficient, but at least it still works.
On the other hand,
if you misdeclare the object as being small when it is actually
large, then you end up in trouble.
For example, if you write
extern BYTE b;
in one file, and
extern BYTE b;
in another, then files that include the first header will think
the object is small and generate “small” code to access it, but
files that include the second header will think it is large.
And if the object turns out to be large after all, the code that
used the first header file will fail pretty spectacularly.
So don’t do that. When you declare a variable, make sure to declare
it accurately. Otherwise the ia64 will catch you in a lie,
and you will pay.