3

I am trying to implement my version of __cxa_allocate_exception and __cxa_free_exception to avoid memory allocation on throw.

So I implemented a memory pool which seems to work fine. But when testing with nested exceptions, the destructor of the exceptions was not called in all cases, and therefore __cxa_free_exception was not called either, causing the memory pool to fill up over time.

See the following example code:

class MyException {
public:
  MyException() {
    std::cout << "MyException constructed." << std::endl;
  }
  ~MyException() {
    std::cout << "MyException destroyed." << std::endl;
  }
};

void * __cxa_allocate_exception(size_t thrown_size)
{
  const auto mem = malloc(thrown_size); //Not part of the example
  std::cout << "allocate: " << mem <<  std::endl;
  return mem;
}

void __cxa_free_exception(void *thrown_object)
{
  std::cout << "free: " << thrown_object << std::endl;

  free(thrown_object); //Not part of the example.
}

void non_rec() {
  try {
    throw MyException();
  } catch(...) {
    try {
      throw MyException();
    } catch(...) {
      //...
    }
  }
}

int main() {
  while(true) {
    non_rec();
    std::cout << "-----------" << std::endl;
  }
}

The output of this program is:

allocate: 0x8cbc20
MyException constructed.
allocate: 0x8cc030
MyException constructed.
MyException destroyed.
free: 0x8cc030
MyException destroyed.
free: 0x8cbc20
-----------
allocate: 0x8cbc20
MyException constructed.
allocate: 0x8cc030
MyException constructed.
MyException destroyed.
free: 0x8cc030
-----------
allocate: 0x8cc030
MyException constructed.
allocate: 0x8cc440
MyException constructed.
MyException destroyed.
free: 0x8cc440
-----------
allocate: 0x8cc440
MyException constructed.
allocate: 0x8cc850
MyException constructed.
MyException destroyed.
free: 0x8cc850

It works correctly the first time. But after that the two exceptions are constructed and allocated in every loop iteration but only one is freed and destroyed.

I am using g++ 5.4.0 on Ubuntu 16.04.

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
Andreas Pasternak
  • 1,250
  • 10
  • 18
  • Check out this [Live Demo](https://wandbox.org/permlink/xueL7yHoNhuHqB52), where no destruction happens at all, not even in the first time! – gsamaras Jun 28 '18 at 08:44
  • In my testing this just crashes, is this something you are actually allowed to do? – Alan Birtles Jun 28 '18 at 08:52
  • Yes, also crashes with me. – Andreas Pasternak Jun 28 '18 at 08:55
  • "to avoid memory allocation on throw" But ... memory isn't allocated on throw; The library creates a memory pool on startup and exceptions are all created in that pool. – UKMonkey Jun 28 '18 at 09:01
  • 1
    looking at the libstdc++ source you need to allocate more than `thrown_size` https://code.woboq.org/gcc/libstdc++-v3/libsupc++/eh_alloc.cc.html – Alan Birtles Jun 28 '18 at 09:06
  • @UKMonkey, this is not true at least for Itanium C++ ABI: https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html#cxx-throw 2.4.2 – Andreas Pasternak Jun 28 '18 at 09:06
  • @AndreasPasternak Keep reading ... https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html#imp-emergency – UKMonkey Jun 28 '18 at 09:09
  • 1
    I'm tempted to say this is just UB because you're overriding a library implementation detail with a reserved identifier. It might be interesting to understand why it fails in this particular way, but I'm not really surprised it doesn't work. – Useless Jun 28 '18 at 09:18
  • I uploaded my full code: https://wandbox.org/permlink/dLyUKXLR1gcwjRoX. If you toggle line 51 the error goes away, but then the memory in the pool is not freed. – Andreas Pasternak Jun 28 '18 at 09:24
  • @UKMonkey: Yes, but I want to get rid of the allocation and the mutex as we are trying to build an application in an safety critical environment. – Andreas Pasternak Jun 28 '18 at 09:25
  • __cxa_free_exception should ideally be called after end of catch but it is not being called. It is called by __cxa_end_catch. –  Jun 28 '18 at 09:31
  • @JamesBond It's up to the compiler to decide when to call it, surely. It knows / determines the lifetime of the `MyException` object. – Paul Sanders Jun 28 '18 at 09:40
  • @AndreasPasternak Two fixes for you below, choose your poison. – Paul Sanders Jun 28 '18 at 09:41

2 Answers2

3

I think the short answer to this is that it's unspecified. Section 18.1.4 of the C++ standard has this to say:

The memory for the exception object is allocated in an unspecified way...

MSVC, for example, allocates it on the stack. Good luck freeing that.

However, it's interesting to look into why the code as written fails (as for other commentators, gcc reports memory corruption when I try to run it) and, as @AlanBirtles says, the answer lies here:

https://code.woboq.org/gcc/libstdc++-v3/libsupc++/eh_alloc.cc.html

If you look at the implementation of __cxa_allocate_exception (line 279), you will see that it does three things that you don't do:

  • it allocates extra space for a header of (private) type __cxa_refcounted_exception
  • it zeroes that header
  • it returns a pointer to the first byte after that header

Then, in __cxa_free_exception, it allows for that pointer adjustment before freeing it.

So it's easy enough to get it to work, just do something like this (or maybe you can tunnel your way through to the declaration of __cxa_refcounted_exception, I think it's on that site somewhere):

#define EXTRA 1024

extern "C" void * __cxa_allocate_exception(size_t thrown_size)
{
  void *mem = malloc (thrown_size + EXTRA);
  std::cout << "allocate: " << mem <<  " (" << thrown_size << ") " << std::endl;
  memset (mem, 0, EXTRA);
  return (char *) mem + EXTRA;
}

extern "C" void __cxa_free_exception(void *thrown_object)
{
  std::cout << "free: " << thrown_object << std::endl;
  char *mem = (char *) thrown_object;
  mem -= EXTRA;
  free (mem);
}

And when I run this at Wandbox, I get:

allocate: 0x1e4c990 (1) 
MyException constructed.
allocate: 0x1e4ddb0 (1) 
MyException constructed.
MyException destroyed.
free: 0x1e4e1b0
MyException destroyed.
free: 0x1e4cd90
-----------
allocate: 0x1e4c990 (1) 
MyException constructed.
allocate: 0x1e4ddb0 (1) 
MyException constructed.
MyException destroyed.
free: 0x1e4e1b0
MyException destroyed.
free: 0x1e4cd90
-----------
allocate: 0x1e4c990 (1) 
MyException constructed.
allocate: 0x1e4ddb0 (1) 
MyException constructed.
MyException destroyed.
free: 0x1e4e1b0
MyException destroyed.
free: 0x1e4cd90
-----------

This doesn't work with clang though, so they must be doing things a different way. Like I say, it's UB, so be careful.

Oliv
  • 17,610
  • 1
  • 29
  • 72
Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • Some related discussion here https://stackoverflow.com/questions/45497684/what-happens-if-throw-fails-to-allocate-memory-for-exception-object –  Jun 28 '18 at 11:05
1

Allocating the correct amount of memory in a similar way to libstdc++ fixes the crashes for me:

#include <iostream>
#include <cstdlib>
#include <exception>
#include <cstring>

class MyException {
public:
  MyException() {
    std::cout << "MyException constructed." << std::hex << (size_t)this << std::endl;
  }
  ~MyException() {
    std::cout << "MyException destroyed." << std::hex << (size_t)this << std::endl;
  }
};

const size_t __cxa_refcounted_exception_size = 16 * sizeof(size_t); // approx sizeof(__cxa_refcounted_exception)

void * __cxa_allocate_exception(size_t thrown_size)
{
  thrown_size += __cxa_refcounted_exception_size;
  const auto mem = malloc(thrown_size);
  std::cout << "allocate: " << mem <<  std::endl;
  memset (mem, 0, __cxa_refcounted_exception_size);
  return (void *)((char *)mem + __cxa_refcounted_exception_size);
}

void __cxa_free_exception(void *thrown_object)
{
  std::cout << "free: " << thrown_object << std::endl;
  char *ptr = (char *) thrown_object - __cxa_refcounted_exception_size;

  free(ptr);
}

void non_rec() {
  try {
    throw MyException();
  } catch(...) {
    try {
      throw MyException();
    } catch(...) {
      //...
    }
  }
}

int main() {
  for (int i=0;i<4;i++) {
    non_rec();
    std::cout << "-----------" << std::endl;
  }
}

You should find the actual value of sizeof(__cxa_refcounted_exception_size) for your platform by including unwind-cxx.h.

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
  • Seems you beat me to the punch... I didn't see this, I was typing up mine. – Paul Sanders Jun 28 '18 at 09:37
  • Thank you! I tried to integrate your solution into my memory pool (https://wandbox.org/permlink/dLyUKXLR1gcwjRoX) but destruction does still not work properly. Any ideas? – Andreas Pasternak Jun 28 '18 at 09:38
  • @AndreasPasternak You didn't do it right. In fact, as far as I can see, you didn't do it at all, is that the right link? – Paul Sanders Jun 28 '18 at 09:43
  • @PaulSanders: I made a small mistake but now it works. Thank you for your help. Here the working version: https://wandbox.org/permlink/gSLww1esDDIErKZD – Andreas Pasternak Jun 28 '18 at 09:52
  • Unfortunately I cannot accept two answers, I therefore accepted the more detailed one. But thank you two for your help! – Andreas Pasternak Jun 28 '18 at 09:55
  • @AndreasPasternak Interesting. I'm not sure why you're getting all those "Freeing exception not from this pool" messages, I don't see anything like that in my code (it would crash if I did). Also, `malloc()` doesn't throw, so you can get rid of your `try` ... `catch` block there (and maybe that will fix the problem). Also, I'd be inclined to apply the pointer manipulation trickery in `__cxa_allocate_exception` and `__cxa_free_exception` to keep your `ExceptionMemoryPool` 'pure', but maybe that's just me. – Paul Sanders Jun 28 '18 at 10:59
  • @PaulSanders yep, I 'd imagine putting a try catch inside the exception handling implementation isn't something the compiler would expect or implement properly – Alan Birtles Jun 28 '18 at 11:38
  • @AlanBirtles Well, he's only doing it in the `ExceptionMemoryPool` constructor so I don't see it as a problem per se, but it shouldn't be there at all. – Paul Sanders Jun 28 '18 at 12:35
  • Thanks for your input! I accidentally did call my own function free, there are the error messages coming from. I also fixed this now. – Andreas Pasternak Jun 28 '18 at 18:33
  • I created a reasonably well-tested memory pool to cover this use-case: https://github.com/ApexAI/static_exception – Andreas Pasternak Jul 10 '18 at 11:58