I’ve been working on a NIC driver project on Pharlap. Due to some reason, it can’t send out any packets. I took some time troubleshooting the problem and finally it turned out to be the incorrect setting of the following hardware register.
struct Registers
{
unsigned long TxDescBaseHi;
unsigned long TxDescBaseLo;
..
};
The register consists of two 32-bit registers which are written by the software driver during initialization to tell the hardware about the starting address of the transmit descriptors. Transmit descriptor is a structure storing the address of the packet to send, along with some control information. The two registers are there to support 64-bit addressing system.
The initialization code is like:
TxDescriptor *m_txRing = m_DMABuffer.Get(SIZE, ALIGNMENT)
..
u64 baseAddr = reinterpret_cast<u64>(m_txRing);
m_pReg->TxDescBaseHi = baseAddr >> 32;
m_pReg->TxDescBaseLo = static_cast<u32>(baseAddr);
The code is simple to be self-describing. However, it’s where the problem is. Since the Pharlap I’m working on is 32-bit, the pointer m_txRing
is 4-byte long. Here are the printed values of these variables.
m_txRing: 0x80236400
m_pReg->TxDescBaseHi:0xFFFFFFFF
m_pReg->TxDescBaseLo:0x80236400
As you can see, the value in high register is not what we expect. 32-bit pointer m_txRing
is sign extended. In C++/C, converting a pointer to an integer is implementation-defined.
"A pointer can be explicitly converted to any integral type large
enough to hold it. The mapping function is implementation-defined."
- C++03
In MSVC/GCC, if you cast a pointer to an integer of a bigger size, the pointer is first sign extended, and the resulting value is then converted to the integer. In this case, 0×80236400
is sign extended to 0xFFFFFFFF80236400
, and since it’s in the range of u64, it’s the final value.
The solution is to use uintptr_t
. uintptr_t
is an alias(typedef
) of an unsigned integer which has the same size of a pointer(Note that intptr_t
/uintptr_t
is optional in C++11). First, convert the pointer to uintptr_t
. There is no extension here. Second, convert the result to target integer.
A few takeaways:
- Don’t convert between pointer and integer unless it’s essential. When the time comes, use
uintptr_t
. Convert the pointer to uintptr_t
first, to avoid possible sign extension. reinterpret_cast
is not guaranteed by the standard to have the same bit pattern as the original although it’s a common practice. Sign extension in this case, is one place where it doesn’t hold, kind of.
"The mapping performed by reinterpret_cast might, or might not,
produce a representation different from the original value. "
- C++03