Sign Extension of C++ Pointer: The Cause of NIC’s Failure to Send Packets

I’ve been working on a NIC driver project on Pharlap. Due to some cause, 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.

// Memory-mapped buffer
 struct Registers
 {
    // High 32 bits of transmit descriptor base address
    unsigned long TxDescBaseHi;
    // Low 32 bits of transmit descriptor base address
    unsigned long TxDescBaseLo;
    ..
 };

The register consists of two 32-bit registers which are written by the software driver during initialzation to tell the hardware about the starting address of the tranmit 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 has a 32-bit CPU(Intel i5-440), the pointer m_txRing is 4-byte long. Here are the printed value 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, 0x80236400 is sign extended to 0xFFFFFFFF80236400, and since it’s in the range of u64, it’s the final value.

uintptr_t

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.

Takeaways

1. 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.

2. reinterpret_cast is not guaranteed by the standard to have the same bit pattern as the orginal 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