Null-Pointer Exception on Return
“So look with your heart, and not with your eyes.
A heart understandsm, a heart never lies.”
— Love Never Dies
Prologue
I guess this starts a new series in my blog, which record my debugging experience on some weird bugs, especially those deepen my understanding of the language. I know nowadays, AI makes writing and debugging code much easier, but I think it is still necessary to have the ability to understand.
So, without further ado, let’s begin.
Let’s Debug
Well, a briefing about the platform and compiler I used. Later for convenience, I will show the debugging process on Windows.
- Windows 11 Pro + Visual Studio 2026 18.9.0
- Ubuntu 20.04 + GCC 13.3.0
And by the way, it is assignment 6 from GAMES101, which is mainly about Bounding Volume Hierarchy (BVH) for ray casting.
The Symptom
It is an access violation, which attempts to read at 0x0. So, a guess is that a NULL pointer get dereferenced somewhere.
1 | Exception thrown at 0x00007FF61B835100 in assignment_06.exe: 0xC0000005: Access violation reading location 0x0000000000000000. |
Here is the code snippet that triggers the exception, marking the line where the exception is thrown.
1 | class BVHAccel |
First analysis
Just by looking at the code above, one can tell this is no trivial bug. The exception does not occur at pointer dereference, but simply a function call. And if you inspect root and ray, none of them is NULL. What the?
If you are more experienced, you may realize there is another pointer involved here, which is this. Well, indeed, if you invoke method on a NULL pointer, an exception will also be thrown. Could it be? Well, not in this case. Since we are already inside BVHAccel::Intersect, this can’t be NULL. And for your reference, invoking a method on a NULL pointer will throw a different exception as given below.
1 | Exception thrown: read access violation. |
So, what could it be? There is no clue in the source code!
Behind the source code
If there is no clue in the source code, let’s dive deeper, into the assembly code.
A tip, in Visual Studio you can check the assembly code with “Go To Disassembly” feature, which is available in the right-click context menu.
Before we continue, here is some reminders of the basis, just in case.
- 1 Byte = 8 bits
- 1 Word = 2 Bytes
- 1 DWord = 2 Word
- 1 QWord = 2 DWord = 4 Word
And for Intel x86 assembly, here are something you need to know. Check x86 Assembly for more information.
- Integer arguments are passed in registers
rcx,rdx,r8andr9. (See x64 calling convention.)mov dest, src: Copies thesrcoperand into thedestoperand, after which both operands contain the same contents.lea dest, src: Load effective address, calculates the address of thesrcoperand and loads it into thedestoperand.ecxcan also be used withrepto repeats operationecxtimes.
Here is the disassembled code of this suspicious function call. We can see that, because the returned struct is too large, BVHAccel::getIntersection returns an address of the value, then copy that to intersection.
1 | isect = BVHAccel::getIntersection(root, ray); |
You may wonder where is root. Well, struct members are represented by offsets. Here root has offset 0 in BVHAccel, so happened to have the same address as this.

Then, why set ecx to 0x38 (56) times? Well, that is the size of struct Intersection.

Now, we can finally locate the source of the exception. It occurs at the last instruction rep movs, where rsi is 0x0!
Ah ha!
Now, things are much clearer. And in fact, the answer is a little too stupid. The returned rax is 0x0, which means… nothing is returned. So, I looked at BVHAccel::getIntersection, and found that I forgot to add return at the end of the function…
Well, you can’t blame the compiler or IDE, as they already warned you.

After this, I changed the inspection severity of it from warning to error. 😅
After adding the missing return, everything is fine.
Reflection
Well, problem solved. But there is another interesting finding.
Except from the warning of “Not all control paths return a value”, another warning is about copy elision.

I mentioned this in C++ Object Lifecycle when talking about return value optimization. So, if copy elision is applicable here, which means no extra copy is needed, does this become a silent bug?
Yes, it may. But it depends on the optimization. If the optimization happens not to touch rax, it may be some random address which is OK to access. But the result is almost never correct, because the instruction to set rax to intersection is never generated.
Epilogue
I didn’t expect this to take so long to write. It turns out I have to review a lot to finish this. Ever since AI coding agents get popular, seldom have I had this leisure to write code and debug by myself. ᓚᘏᗢ









