Darwin vs exceptions

So I couldn’t get exceptions to work on PPC darwin. After much digging and confusion, there seem
to be two separate issues.

The gcc testsuite is running the version of the unwinding code that was built with the local (llvm-)gcc,
which doesn’t work because nobody has implemented builtin_return_address for that target.
So that’s one problem.

More seriously, the version of the unwinding code installed on the system doesn’t work either, for a
different reason (and also doesn’t work on x86 Darwin). It turns out the installed code has the
following patch, which is not in the llvm-gcc source (or Apple’s gcc sources for that matter; libgcc
lives in a separate repository these days):

Index: gcc/unwind-dw2.c

So I couldn’t get exceptions to work on PPC darwin. After much digging and confusion, there seem
to be two separate issues.

Ok.

The gcc testsuite is running the version of the unwinding code that was built with the local (llvm-)gcc,
which doesn’t work because nobody has implemented builtin_return_address for that target.
So that’s one problem.

That’s an easy problem. I just implemented __builtin_return_address on PPC, at least builtin_return_address(0), please let me know if that is sufficient.

More seriously, the version of the unwinding code installed on the system doesn’t work either, for a
different reason (and also doesn’t work on x86 Darwin). It turns out the installed code has the
following patch, which is not in the llvm-gcc source (or Apple’s gcc sources for that matter; libgcc
lives in a separate repository these days):

Yuck :slight_smile:

Index: gcc/unwind-dw2.c

===================================================================
— gcc/unwind-dw2.c (revision 124749)
+++ gcc/unwind-dw2.c (working copy)
@@ -1526,7 +1526,7 @@
static inline _Unwind_Ptr
uw_identify_context (struct _Unwind_Context *context)
{

  • return _Unwind_GetIP (context);
  • return _Unwind_GetCFA (context);
    }

I’m uncertain of the merit of this patch, but it’s in shipping Leopard and works with gcc, so we’re
stuck with it.

Right, this is effectively part of the darwin ABI now.

The effect of this is that llvm’s strategy of making every C++ selector claim that it has a catch-all
handler at the end, as described here
http://llvm.org/docs/ExceptionHandling.html#restrictions
does not work in this environment. Claiming that a function has a catch-all handler when it does
not causes the unwinder to go into a loop.

Okay, you’re beyond my knowledge here :slight_smile:

I tried simply removing the code that does this:

Index: cp/except.c

— cp/except.c (revision 44663)
+++ cp/except.c (working copy)
@@ -104,7 +104,7 @@
lang_eh_runtime_type = build_eh_type_type;
lang_protect_cleanup_actions = &cp_protect_cleanup_actions;
/* LLVM local */

  • lang_eh_catch_all = return_null_tree;
    +/* lang_eh_catch_all = return_null_tree;*/
    }

/* Returns an expression to be executed if an unhandled exception is

and that works well for me; i.e., the gcc tests that exercise exceptions work as well with the
two patches above (on x86) as they did before.

Ok, cool :slight_smile: Duncan, Anton, what do you guys think about this?

Now I need some help:

  • Why was C++ claiming that every selector has a catch-all handler? The documentation above just says

the C++ personality function will terminate the program if it detects that unwinding the exception only results in matches with cleanups.
Isn’t that what’s supposed to happen if there’s no handler anywhere?

  • What is the right thing for Linux? Is the deletion above safe there?

Unsure,

-Chris

Hi Dale,

- Why was C++ claiming that every selector has a catch-all handler?

this is easy: because the semantics of invoke require it. Yes, really.
If unwinding reaches an invoke then control is required to jump to the
unwind basic block. At first I thought this probably wouldn't matter -
that it would be OK to not jump to the landing pad if the exception was
not being caught by it - and didn't implement it, but several eh failures
in the LLVM testsuite could be tracked down to it: the optimizers really
do exploit this property of invoke - it is quite subtle. You typically see
it when code is massively inlined into one big flat main function. Then I
tried to implement it by pushing a "cleanup" at the end of the exception
list. But the unwinder treats cleanups specially, and ignores them if during
unwinding it only sees cleanups all the way up to the top - in short there
were testsuite failures with this approach. So the only thing to do was
to push a catch-all on to the end of the list.

The documentation above just says
the C++ personality function will terminate the program if it detects
that unwinding the exception only results in matches with cleanups.
Isn't that what's supposed to happen if there's no handler anywhere?

It is, and it is what will happen: with the current code you will unwind
into each landing pad and be rethrown until you get to the top. Then the
program will be terminated. Yes, this means that you get terminated slightly
more slowly than with gcc, however I don't see how to obtain correct invoke
semantics (and thus: correctly working programs) without it.

- What is the right thing for Linux? Is the deletion above safe there?

No, and it's not safe anywhere else either, though most of the time it
doesn't make any difference.

Ciao,

Duncan.

Hi Chris,

... Claiming that a function has a
> catch-all handler when it does
> not causes the unwinder to go into a loop.

this is the bit I don't understand. Why does it go
into a loop? How can the unwinder possibly know that
the original code did not have a catch-all, since we
tell it which catches there are and we say: there is
a catch-all!

> - lang_eh_catch_all = return_null_tree;
> +/* lang_eh_catch_all = return_null_tree;*/

Ok, cool :slight_smile: Duncan, Anton, what do you guys think about this?

This is wrong - it breaks the semantics of invoke and causes
eh to not work properly in some cases (see my email to Dale).

Ciao,

Duncan.

Chris,

That's an easy problem. I just implemented __builtin_return_address
on PPC, at least builtin_return_address(0), please let me know if that
is sufficient.

There are bunch of another builtins required for eh to execute properly.
The 3 most important one are:
- eh_return
- unwind_init
- dwarf_cfa

The first two precisely match to the gcc's ones, the third - slightly
differs, because is catches some extra information from the frontend.

Hi Chris,

... Claiming that a function has a

catch-all handler when it does
not causes the unwinder to go into a loop.

this is the bit I don't understand. Why does it go
into a loop? How can the unwinder possibly know that
the original code did not have a catch-all, since we
tell it which catches there are and we say: there is
a catch-all!

The unwinder works by doing a stack crawl to find a handler. Since we're telling it every eh-selector is a catch-all handler, it finds one immediately (that is not there) and caches where it thinks it is. That code (cleanup or whatever) is executed and ends by reaching Unwind_Resume, which tries to resume at where it thinks the handler is. Which is back at the same code from which we reached Unwind_Resume. This worked OK when we were caching the IP because the IP of the throw and Unwind_Resume call were different, but when caching the CFA they are the same, which confuses Unwind_Resume.
(This is darwin's unwinder but I think the patch I quoted is the only relevant change to generic gcc code.
Could be wrong.)

- lang_eh_catch_all = return_null_tree;
+/* lang_eh_catch_all = return_null_tree;*/

Ok, cool :slight_smile: Duncan, Anton, what do you guys think about this?

This is wrong - it breaks the semantics of invoke and causes
eh to not work properly in some cases (see my email to Dale).

Well, it works a lot better on Darwin than what's there now. I believe telling the unwinder there is a cleanup is sufficient to get control always transferred to the secondary label on the invoke.

Could you point me to an example that should fail?

Hi Dale,

- Why was C++ claiming that every selector has a catch-all handler?

this is easy: because the semantics of invoke require it. Yes, really.
If unwinding reaches an invoke then control is required to jump to the
unwind basic block. At first I thought this probably wouldn't matter -
that it would be OK to not jump to the landing pad if the exception was
not being caught by it - and didn't implement it, but several eh failures
in the LLVM testsuite could be tracked down to it: the optimizers really
do exploit this property of invoke - it is quite subtle. You typically see
it when code is massively inlined into one big flat main function. Then I
tried to implement it by pushing a "cleanup" at the end of the exception
list. But the unwinder treats cleanups specially, and ignores them if during
unwinding it only sees cleanups all the way up to the top - in short there
were testsuite failures with this approach. So the only thing to do was
to push a catch-all on to the end of the list.

OK, playing around with the testsuite it appears there's a bug in llvm's inliner with EH, which is probably what's causing the effect you're talking about. Suppose we have

#include <cstdio>
class A {
public:
   A() {}
   ~A() {}
};
void f() {
   A a;
   throw 5.0;
}
main() {
   try {
     f();
    } catch(...) { printf("caught\n"); }
}

The IR for f correctly has the throw call reaching the landing pad, which cleans up 'a' and then calls Unwind_Resume. Inlining g into f naively copies this structure which is wrong; we do not want to call Unwind_Resume in this case because there is a real handler in the same function. See the code produced by gcc -O3. I think what you did is make this incorrect Unwind_Resume work (on your OS), but that's not the right way to fix this.

Hi Dale,

> this is the bit I don't understand. Why does it go
> into a loop? How can the unwinder possibly know that
> the original code did not have a catch-all, since we
> tell it which catches there are and we say: there is
> a catch-all!

The unwinder works by doing a stack crawl to find a handler. Since
we're telling it every eh-selector is a catch-all handler, it finds
one immediately (that is not there)

I'm not sure what you mean: there is a handler: it just rethrows
the exception. This is not very interesting of course, but it's
still a handler. Or do you mean something different when you talk
of a handler?

and caches where it thinks it
is. That code (cleanup or whatever) is executed and ends by reaching
Unwind_Resume, which tries to resume at where it thinks the handler
is.

I also don't understand this: why is Unwind_Resume looking at where
the exception's handler is? The handler used for the caught exception
should be irrelevant to it. It sounds like the unwinder thinks that
the Unwind_Resume is in the same eh region as the original throwing
call.

Which is back at the same code from which we reached
Unwind_Resume. This worked OK when we were caching the IP because
the IP of the throw and Unwind_Resume call were different, but when
caching the CFA they are the same, which confuses Unwind_Resume.

How is catch-all then rethrow supposed to work then? Are you sure
that this isn't simply a bug in where labels are placed etc, the
frame info or the exception table setup etc?

(This is darwin's unwinder but I think the patch I quoted is the only
relevant change to generic gcc code.
Could be wrong.)

>>> - lang_eh_catch_all = return_null_tree;
>>> +/* lang_eh_catch_all = return_null_tree;*/
>
>> Ok, cool :slight_smile: Duncan, Anton, what do you guys think about this?
>
> This is wrong - it breaks the semantics of invoke and causes
> eh to not work properly in some cases (see my email to Dale).

Well, it works a lot better on Darwin than what's there now. I
believe telling the unwinder there is a cleanup is sufficient to get
control always transferred to the secondary label on the invoke.

Are you saying that the Darwin unwinder doesn't have the special
cleanup logic? If so you could indeed push a cleanup rather than
a catch-all on Darwin.

Could you point me to an example that should fail?

Sure - run the testsuite on linux with -enable-correct-eh-support
changed to -enable-eh in Makefile.programs. Then apply your patch,
run again and analyse the failures. I will do this and send you
a reduced example (I used to have some but I don't remember what
I did with them).

Ciao,

Duncan.

Hi Dale,

this is the bit I don't understand. Why does it go
into a loop? How can the unwinder possibly know that
the original code did not have a catch-all, since we
tell it which catches there are and we say: there is
a catch-all!

The unwinder works by doing a stack crawl to find a handler. Since
we're telling it every eh-selector is a catch-all handler, it finds
one immediately (that is not there)

I'm not sure what you mean: there is a handler: it just rethrows
the exception. This is not very interesting of course, but it's
still a handler. Or do you mean something different when you talk
of a handler?

I'm following the notation in comments in the unwinding code
(gcc/unwind*, especially unwind.inc, and libstdc++/libsupc++/eh*,
especially eh_personality.cc). "handler" is used consistently to mean
something that actually handles the thrown exception, a catch-block
for that thrown type. Code that just calls destructors and resumes is
a "cleanup", and is handled differently in the tables and code.

Linux's unwinder uses the same code, doesn't it? I bet if you make the same
change in uw_identify_context that we have you would have the same problem.

and caches where it thinks it
is. That code (cleanup or whatever) is executed and ends by reaching
Unwind_Resume, which tries to resume at where it thinks the handler
is.

I also don't understand this: why is Unwind_Resume looking at where
the exception's handler is?

I don't know the 'why' of any of this; I didn't write it, nor did anybody at Apple.

The handler used for the caught exception
should be irrelevant to it. It sounds like the unwinder thinks that
the Unwind_Resume is in the same eh region as the original throwing
call.

Yes, exactly, and when you're using CFAs rather than IPs to identify it, it is.
The code in question is in Unwind_RaiseException_Phase2, reached from
Unwind_Resume:
       /* Identify when we've reached the designated handler context. */
       match_handler = (uw_identify_context (context) == exc->private_2
                        ? _UA_HANDLER_FRAME : 0);

Which is back at the same code from which we reached
Unwind_Resume. This worked OK when we were caching the IP because
the IP of the throw and Unwind_Resume call were different, but when
caching the CFA they are the same, which confuses Unwind_Resume.

How is catch-all then rethrow supposed to work then?

It isn't. Catch-all (corresponding to "catch(...)" ) ends the unwinding.

Are you sure
that this isn't simply a bug in where labels are placed etc, the
frame info or the exception table setup etc?

Yes, comparing gcc and llvm output has made me quite sure of that.

(This is darwin's unwinder but I think the patch I quoted is the only
relevant change to generic gcc code.
Could be wrong.)

- lang_eh_catch_all = return_null_tree;
+/* lang_eh_catch_all = return_null_tree;*/

Ok, cool :slight_smile: Duncan, Anton, what do you guys think about this?

This is wrong - it breaks the semantics of invoke and causes
eh to not work properly in some cases (see my email to Dale).

Well, it works a lot better on Darwin than what's there now. I
believe telling the unwinder there is a cleanup is sufficient to get
control always transferred to the secondary label on the invoke.

Are you saying that the Darwin unwinder doesn't have the special
cleanup logic? If so you could indeed push a cleanup rather than
a catch-all on Darwin.

What "special cleanup logic"?