When gcc generates code that handles exceptions, it produces tables that describe how to unwind the stack. These tables are found in the .eh_frame section. The format of the .eh_frame section is very similar to the format of a DWARF .debug_frame section. Unfortunately, it is not precisely identical. I don’t know of any documentation which describes this format. The following should be read in conjunction with the relevant section of the DWARF standard, available from http://dwarfstd.org.

The .eh_frame section is a sequence of records. Each record is either a CIE (Common Information Entry) or an FDE (Frame Description Entry). In general there is one CIE per object file, and each CIE is associated with a list of FDEs. Each FDE is typically associated with a single function. The CIE and the FDE together describe how to unwind to the caller if the current instruction pointer is in the range covered by the FDE.

There should be exactly one FDE covering each instruction which may be being executed when an exception occurs. By default an exception can only occur during a function call or a throw. When using the -fnon-call-exceptions gcc option, an exception can also occur on most memory references and floating point operations. When using -fasynchronous-unwind-tables, the FDE will cover every instruction, to permit unwinding from a signal handler.

The general format of a CIE or FDE starts as follows:

  1. Length of record. Read 4 bytes. If they are not 0xffffffff, they are the length of the CIE or FDE record. Otherwise the next 64 bits holds the length, and this is a 64-bit DWARF format. This is like .debug_frame.
  2. A 4 byte ID. For a CIE this is 0. For an FDE it is the byte offset from this field to the start of the CIE with which this FDE is associated. The byte offset goes to the length record of the CIE. A positive value goes backward; that is, you have to subtract the value of the ID field from the current byte position to get the CIE position. This differs from .debug_frame in that the offset is relative rather than being an offset into the .debug_frame section.

A CIE record continues as follows:

  1. 1 byte CIE version. As of this writing this should be 1 or 3.
  2. NUL terminated augmentation string. This is a sequence of characters. Very old versions of gcc used the string “eh” here, but I won’t document that. This is described further below.
  3. Code alignment factor, an unsigned LEB128 (LEB128 is a DWARF encoding for numbers which I won’t describe here). This should always be 1 for .eh_frame.
  4. Data alignment factor, a signed LEB128. This is a constant factored out of offset instructions, as in .debug_frame.
  5. The return address register. In CIE version 1 this is a single byte; in CIE version 3 this is an unsigned LEB128. This indicates which column in the frame table represents the return address.

The next fields of the CIE depend on the augmentation string.

  • If the augmentation string starts with ‘z’, we now find an unsigned LEB128 which is the length of the augmentation data, rounded up so that the CIE ends on an address boundary. This is used to skip to the end of the augmentation data if an unrecognized augmentation character is seen.
  • If the next character in the augmentation string is ‘L’, the next byte in the CIE is the LSDA (Language Specific Data Area) encoding. This is a DW_EH_PE_xxx value (described later). The default is DW_EH_PE_absptr.
  • If the next character in the augmentation string is ‘R’, the next byte in the CIE is the FDE encoding. This is a DW_EH_PE_xxx value. The default is DW_EH_PE_absptr.
  • The character ‘S’ in the augmentation string means that this CIE represents a stack frame for the invocation of a signal handler. When unwinding the stack, signal stack frames are handled slightly differently: the instruction pointer is assumed to be before the next instruction to execute rather than after it.
  • If the next character in the augmentation string is ‘P’, the next byte in the CIE is the personality encoding, a DW_EH_PE_xxx value. This is followed by a pointer to the personality function, encoded using the personality encoding. I’ll describe the personality function some other day.

The remaining bytes are an array of DW_CFA_xxx opcodes which define the initial values for the frame table. This is then followed by DW_CFA_nop padding bytes as required to match the total length of the CIE.

An FDE starts with the length and ID described above, and then continues as follows.

  1. The starting address to which this FDE applies. This is encoded using the FDE encoding specified by the associated CIE.
  2. The number of bytes after the start address to which this FDE applies. This is encoded using the FDE encoding.
  3. If the CIE augmentation string starts with ‘z’, the FDE next has an unsigned LEB128 which is the total size of the FDE augmentation data. This may be used to skip data associated with unrecognized augmentation characters.
  4. If the CIE does not specify DW_EH_PE_omit as the LSDA encoding, the FDE next has a pointer to the LSDA, encoded as specified by the CIE.

The remaining bytes in the FDE are an array of DW_CFA_xxx opcodes which set values in the frame table for unwinding to the caller.

The DW_EH_PE_xxx encodings describe how to encode values in a CIE or FDE. The basic encoding is as follows:

  • DW_EH_PE_absptr = 0x00: An absolute pointer. The size is determined by whether this is a 32-bit or 64-bit address space, and will be 32 or 64 bits.
  • DW_EH_PE_omit = 0xff: The value is omitted.
  • DW_EH_PE_uleb128 = 0x01: The value is an unsigned LEB128.
  • DW_EH_PE_udata2 = 0x02, DW_EH_PE_udata4 = 0x03, DW_EH_PE_udata8 = 0x04: The value is stored as unsigned data with the specified number of bytes.
  • DW_EH_PE_signed = 0x08: A signed number. The size is determined by whether this is a 32-bit or 64-bit address space. I don’t think this ever appears in a CIE or FDE in practice.
  • DW_EH_PE_sleb128 = 0x09: A signed LEB128. Not used in practice.
  • DW_EH_PE_sdata2 = 0x0a, DW_EH_PE_sdata4 = 0x0b, DW_EH_PE_sdata8 = 0x0c: The value is stored as signed data with the specified number of bytes. Not used in practice.

In addition the above basic encodings, there are modifiers.

  • DW_EH_PE_pcrel = 0x10: Value is PC relative.
  • DW_EH_PE_textrel = 0x20: Value is text relative.
  • DW_EH_PE_datarel = 0x30: Value is data relative.
  • DW_EH_PE_funcrel = 0x40: Value is relative to start of function.
  • DW_EH_PE_aligned = 0x50: Value is aligned: padding bytes are inserted as required to make value be naturally aligned.
  • DW_EH_PE_indirect = 0x80: This is actually the address of the real value.

If you follow all that, and also read up on .debug_frame, then you have enough information to unwind the stack at runtime, e.g. to implement glibc’s backtrace function. Later I’ll describe the LSDA and the personality function, which work together to implement exception catching on top of stack unwinding.






8 responses to “.eh_frame”

  1. bjorntopel Avatar

    Excellent post, Ian! Thanks for putting this into print.

  2. DGentry Avatar

    The level of detail here is very interesting, thank you for writing it.

    Over time I think I’ve used the simple __builtin_return_address() more than anything else, to log the address of the caller(s). If I need something more extensive, I end up using libunwind in order to get a backtrace + various register values.

    To me the glibc backtrace() functionality is at the awkward midpoint of being a bit too heavy for the light stuff, and a bit too light for the heavy stuff.

  3. Fafnir Avatar

    Unfortunately, exception handling data isn’t very well documented (as you pointed out). Thanks for the article that ties some parts of it together. You mentioned the DWARF documentation, which describes the CIE and FDE entries. For further reading, may I suggest the following two documents:

    1) This is a good document on the layout of the exception tables (GCC_except_table in the .s file):


    2) This is a document on the EH ABI for Intel:


    I used these three documents heavily to understand what GCC and LLVM output for exceptions.

  4. br Avatar

    Thanks for writing this up. Very timely since I was just talking about how hairy backtracing is on x86_64 (sans frame-pointer) vs ia32 with a coworker yesterday, and then this shows up in my feeds.

    I was also reading thru the amd64 abi and it contains some info about the .eh_frame extensions to .debug_frame that you describe:
    sections 3.7 and 3.2.

  5. Wong.KwongYuan Avatar

    Thanks for your article, helps people in system software greatly !

  6. vgupta Avatar

    Hi Ian,

    Thanks for superb introduction to this arcane topic specially .eh_frame vs. .debug_frame is confusing as hell. I do have a question w.r.t FDE entry’s CIE-ID field.

    Do I understand you correctly, that for .debug_frame it encodes the offset from start of .debug_frame to start of relevant CIE entry – as opposed to .eh_frame where it encodes the offset from FDE entry’s current location to CIE entry.

    Actually the context is ARC GNU toolchain (from Synopsys) and at present our .debug_frame has .eh_frame’ish encoding for CIE_ID but somehow our tools guys think I’m not parsing the spec (or your blog) correctly.

    I understand that this blog might not be right forum to discuss this – if so shall I post this queston on binutils mailing list

    .section .debug_frame,””,@progbits
    .4byte @.LECIE0-@.LSCIE0 –> start of CIE

    .4byte @.LEFDE0-@.LASFDE0 –> start of FDE
    .4byte @.Lframe0 –> this seems WRONG to me, needs to be 0 IMHO

  7. Ian Lance Taylor Avatar

    In .eh_frame the ID for an FDE is a relative offset. In .debug_frame it’s normally an absolute address, as computed by some relocation at link time.

  8. […] Ian Lance Taylor ? ?? .eh_frame ?????? […]

Leave a Reply