Clang does not produce full .debug_aranges section with ThinLTO

Consider the following example:

$ cat test.c 
void bar()
{
}
void foo()
{
        bar();
}
int main()
{
        foo();
}

And now if you compile it with -gdwarf-aranges clang/llvm correctly generates it:

clang -gdwarf-aranges -g -O3 -gdwarf-4 test.c
$ eu-readelf -waranges a.out  | fgrep bar
   +0x0000000000001120 <bar>..+0x0000000000001142 <main+0x2>
$ objdump -h -j .debug_aranges a.out 

a.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
 24 .debug_aranges 00000120  0000000000000000  0000000000000000  00003040  2**4
                  CONTENTS, READONLY, DEBUGGING, OCTETS

However if you enables ThinLTO it does not:

$ clang -flto=thin -gdwarf-aranges -g -O3 -gdwarf-4 test.c
$ eu-readelf -waranges a.out  | fgrep bar
$ objdump -h -j .debug_aranges a.out 

a.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
 24 .debug_aranges 000000f0  0000000000000000  0000000000000000  00003040  2**4
                  CONTENTS, READONLY, DEBUGGING, OCTETS

Can someone give more details where I should look?
Or maybe “it works as designed”?

Thanks in advance,
Azat.

It looks like in your example foo and bar are completely removed from the program with thinLTO enabled and are then not present in the debug info, which looks like reasonable behaviour to me. You can get this to occur in a non-LTO build if you mark those functions static.

Thank you for the answer!
Yes, you are right for this particular case!

However there is still some issue (and actually I came up to this from a ~2GB binary that has almost empty .debug_aranges, but just trying to reduce the scope)

Like I wrote you are right about my first example, since .debug_info also empty for it.

However if I will add __attribute__((optnone)):

void __attribute__((optnone)) bar()
{
}
void __attribute__((optnone)) foo()
{
        bar();
}
int main()
{
        foo();
}

And compile it with -flto=thin:

clang -flto=thin -gdwarf-aranges -g -O3 test.c

Entries for foo and bar should exist, right? However they are not:

$ eu-readelf -waranges a.out  | fgrep -c -e foo -e bar
0

However .debug_info contains entry for them:

$ llvm-dwarfdump --debug-info a.out | fgrep -e foo -e bar
                DW_AT_name      ("bar")
                DW_AT_name      ("foo")

Ah okay, thanks for the new reproducer. Digging into clang:

It looks like the llvm flag generate-arange-section is the internal flag that controls whether aranges are emitted or not (in DwarfDebug.cpp).

When thinLTO is not used, the flag -gdwarf-aranges causes the clang driver to set the generate-arange-section flag mentioned above (in Clang.cpp). However, it looks like when thinLTO is enabled generate-arange-section is not getting set in / passed to lld.

I’m not very familiar with aranges or the interaction with thinLTO but that does sound like a bug to me - @pogo59 / @dblaikie would you agree?

Yep. Not an uncommon bug where flags aren’t serialised to IR. Either you can fix/workaround it by passing certain compile-time flags to your linker invocation (this might require some flag fixes/support) or the more invasive work of adding a new flag to the dicompileunit ir.

But more generally: why are you using aranges? Generally I would encourage folks not to use them and fix any tooling that requires them - the ranges on CUs are equivalent and don’t take up extra space/require extra flags.

Thank you for your answers!

When thinLTO is not used, the flag -gdwarf-aranges causes the clang driver to set the generate-arange-section flag mentioned above (in Clang.cpp). However, it looks like when thinLTO is enabled generate-arange-section is not getting set in / passed to lld.

Actually the flag is passed, and you cannot specify this flag by yourself:

$ clang -flto=thin -mllvm -generate-arange-section -gdwarf-aranges -g -O3 test.c
clang (LLVM option parsing): for the --generate-arange-section option: may only occur zero or one times!

I’ve looked at the llvm code, and looks like the problem is here - llvm-project/BackendUtil.cpp at faad567589a3de43b02a4cdb27fb00f79b567d4c · llvm/llvm-project · GitHub, RequiresCodeGen is false, and that’s why the section is not emitted.

Yep. Not an uncommon bug where flags aren’t serialised to IR. Either you can fix/workaround it by passing certain compile-time flags to your linker invocation

Simply passing -mllvm -generate-arange-section does not work (as you can see above)

(this might require some flag fixes/support)

I’m looking at the llvm code right now… If you can point to a specific place or share idea in more details I can try.

or the more invasive work of adding a new flag to the dicompileunit ir.

I guess you need to be familiar more then I am with the llvm to understand this.

But more generally: why are you using aranges?

It is used for custom Dwarf parser in ClickHouse - ClickHouse/Dwarf.cpp at master · ClickHouse/ClickHouse · GitHub (well, not that custom, it is based on folly’s implementation - folly/Dwarf.cpp at main · facebook/folly · GitHub), to symbolize the stack trace on exceptions.

Generally I would encourage folks not to use them and fix any tooling that requires them - the ranges on CUs are equivalent and don’t take up extra space/require extra flags

You mean .debug_ranges ?
But I need to get CU, I could not do this without .debug_aranges

But anyway if there is option to generate aranges, it is better to make it work even in case of LTO.

Simply passing -mllvm -generate-arange-section does not work (as you can see above)

You’d need to split up the compile and link steps, e.g.

$ clang -flto=thin -g -O3 test.cpp -o test.o
$ ld.lld <all your linker flags> -mllvm -generate-arange-section -o test

That looks like it’s working for me:

$ llvm-dwarfdump --debug-aranges test
test:	file format elf64-x86-64

.debug_aranges contents:
Address Range Header: length = 0x0000004c, format = DWARF32, version = 0x0002, cu_offset = 0x00000000, addr_size = 0x08, seg_size = 0x00
[0x0000000000201730, 0x0000000000201736)
[0x0000000000201740, 0x000000000020174b)
[0x0000000000201750, 0x000000000020175d)

To get the “<all your linker flags>” I ran clang -flto=thin -gdwarf-4 -gdwarf-aranges test.o -o test -fuse-ld=lld -### and copy-pasted the linker command it spits out (I’m not sure if there’s a better way or not).

FYI DICompileUnit is a metadata node in the IR that holds various info and flags, the class definition here in DebugInfoMetadata.h.

Indeed you are right!

So here is a simple patch that will do the trick. What do you think?

commit e3bf84654472ddd16f8848588d9e74b8f528f7b6 (HEAD -> dwarf-aranges)
Author: Azat Khuzhin <a3at.mail@gmail.com>
Date:   Wed Aug 31 18:23:12 2022 +0200

    clang: fix generation of .debug_aranges with LTO

diff --git a/clang/lib/Driver/ToolChains/CommonArgs.cpp b/clang/lib/Driver/ToolChains/CommonArgs.cpp
index 3b084dfdbfa2..c21382e17bec 100644
--- a/clang/lib/Driver/ToolChains/CommonArgs.cpp
+++ b/clang/lib/Driver/ToolChains/CommonArgs.cpp
@@ -506,6 +506,9 @@ void tools::addLTOOptions(const ToolChain &ToolChain, const ArgList &Args,
             Suffix,
         Plugin);
     CmdArgs.push_back(Args.MakeArgString(Plugin));
+  } else if (Args.hasArg(options::OPT_gdwarf_aranges)) {
+    CmdArgs.push_back(Args.MakeArgString("-mllvm"));
+    CmdArgs.push_back(Args.MakeArgString("-generate-arange-section"));
   }
 
   // Try to pass driver level flags relevant to LTO code generation down to

Or at github - clang: fix generation of .debug_aranges with LTO · azat-archive/llvm-project@e3bf846 · GitHub

Although I’m pretty sure it is an ugly hack…

Oh, cool, that seems reasonable. I’m always somewhat tentative about reviewing flag changes or additions though as I feel that I don’t have the experience/broad view to know what’s best, so I’ll have to defer to others for that one.

In the same way a consumer has to walk all the .debug_aranges to build up a mapping, they can walk all the CUs in .debug_info, read the ranges data on the CU node (skipping/not reading the rest of the details in .debug_info) - it’s not substantially more costly to do this (it does require reading one DIE and it’s attributes etc).

I’ve encouraged this in a few places/times and generally it’s why we don’t have aranges on by default in clang because it’s redundant with CU-level ranges. I do hope/plan to see if we can deprecate aranges in a future version of dwarf and at every opportunity I’m encouraging folks to fix consumers to use CU-level ranges when present to avoid dependence on aranges.

The fix direction you folks are discussing sounds approximately right, I think, but haven’t looked closely - and I wouldn’t object to the patch, but equally would encourage anyone hitting issues with aranges that some time would be well spent migrating away from them rather than fixing bugs in clangs support for them.

In the same way a consumer has to walk all the .debug_aranges to build up a mapping, they can walk all the CUs in .debug_info, read the ranges data on the CU node (skipping/not reading the rest of the details in .debug_info) - it’s not substantially more costly to do this (it does require reading one DIE and it’s attributes etc).

Okay, I will need to do some testing then, thank you for sharing!

The fix direction you folks are discussing sounds approximately right, I think, but haven’t looked closely - and I wouldn’t object to the patch, but equally would encourage anyone hitting issues with aranges that some time would be well spent migrating away from them rather than fixing bugs in clangs support for them.

I will submit it as-is right now - https://reviews.llvm.org/D133092 (to get feedback earlier)
But I also need to add some tests