I was responding in comments, but it doesn’t allow me to use links, so here’s the long version:
cmp [exc], exc is the solution to the problem. It’s there because that’s what the JIT generates to do the null test.
Yes, you are missing something. Methods need to be marked as virtual for them to be virtual methods – this is separate from emitting the callvirt instruction.
I’m not sure I understand your example (why are you calling ToString() on b when b is already a string?). It seems to me that what you want can be handled through a method that tests for null and returns either the result of ToString() or the appropriate default value (String.Empty seems to be the logical one in this case).