18

Our project (C++, Linux, gcc, PowerPC) consists of several shared libraries. When releasing a new version of the package, only those libs should change whose source code was actually affected. With "change" I mean absolute binary identity (the checksum over the file is compared. Different checksum -> different version according to the policy). (I should mention that the whole project is always built at once, no matter if any code has changed or not per library).

Usually this can by achieved by hiding private parts of the included Header files and not changing the public ones.

However, there was a case where a mere delete was added to the destructor of a class TableManager (in the TableManager.cpp file!) of library libTableManager.so, and yet the binary/checksum of library libB.so (which uses class TableManager ) has changed.

TableManager.h:

class TableManager 
{
public:
    TableManager();
    ~TableManager();
private:
    int* myPtr;
}

TableManager.cpp:

TableManager::~TableManager()
{
    doSomeCleanup();
    delete myPtr;     // this delete has been added
}

By inspecting libB.so with readelf --all libB.so, looking at the .dynsym section, it turned out that the length of all functions, even the dynamically used ones from other libraries, are stored in libB! It looks like this (length is the 668 in the 3rd column):

527: 00000000 668 FUNC GLOBAL DEFAULT UND _ZN12TableManagerD1Ev

So my questions are:

  1. Why is the length of a function actually stored in the client lib? Wouldn't a start address be sufficient?
  2. Can this be suppressed somehow when compiling/linking of libB.so (kind of "stripping")? We would really like to reduce this degree of dependency...
Ingmar
  • 2,361
  • 3
  • 19
  • 33

2 Answers2

11

Bingo. It is actually kind of a "bug" in binutils which they found and fixed in 2008. The size information is actually not useful!

What Simon Baldwin wrote in the binutils mailing list describes exactly the problem ( emphases by me):

Currently, the size of an undefined ELF symbol is copied out of the object file or DSO that supplies the symbol, on linking. This size is unreliable, for example in the case of two DSOs, one linking to the other. The lower- level DSO could make an ABI-preserving change that alters the symbol size, with no hard requirement to rebuild the higher-level DSO. And if the higher- level DSO is rebuilt, tools that monitor file checksums will register a change due to the altered size of the undefined symbol, even though nothing else about the higher-level DSO has altered. This can lead to unnecessary and undesirable rebuild and change cascades in checksum-based systems.

We have the problem with an older system (binutils 2.16). I compared it with version 2.20 on the desktop system and - voilà - the lengths of shared global symbols were 0:

157: 00000000     0 FUNC    GLOBAL DEFAULT  UND _ZN12TableManagerD1Ev
158: 00000000     0 FUNC    GLOBAL DEFAULT  UND _ZNSs6assignERKSs@GLIBCXX_3.4 (2)
159: 00000000     0 FUNC    GLOBAL DEFAULT  UND sleep@GLIBC_2.0 (6)
160: 00000000     0 FUNC    GLOBAL DEFAULT  UND _ZN4Gpio11setErrorLEDENS_

So I compared both binutils source codes, and - voilà again - there is the fix which Alan suggested in the mailing list:

enter image description here

Maybe we just apply the patch and recompile binutils since we need to stay with the olderish platform. Thanks for your patience.

Ingmar
  • 2,361
  • 3
  • 19
  • 33
8

You'd need to read through the code for the loader to be sure, but I think in this case we can make a fairly reasonable guess about what that length field is intended to accomplish.

The loader needs to take all the functions that are going to be put into the process, and map them to memory addresses. So, it gives the first function an address. Then, the second comes after the end of the first -- but to know "the end of the first", it needs to know how long the first function is.

I can see two ways for it to approach getting that length: it can either have it encoded in the file (as you'd seen it is in ELF) or else it can open the file that contains the function, and get the length from there.

The latter seems (to me) to have two fairly obvious disadvantages. The first is speed -- opening all those extra files, parsing their headers, etc., just to get the lengths of the functions is almost certainly slower than reading an extra four bytes for each function from the current file. The second is convenience: as long as you don't call any of the functions in a file, you don't need that file to be present at all. If you read the lengths directly from the file (e.g., like Windows normally does with DLLs) you'd need that file to be present on the target system, even if it's never actually used.

Edit: Since some people apparently missed the (apparently too-) subtle implication of "intended to accomplish", let me be entirely clear: I'm reasonably certain this field is not (and never has been) actually used.

Anybody who thinks that makes this answer wrong, however, needs to go back to programming 101 and learn the difference between an interface and an implementation.

In this case, the file format defines an interface -- a set of capabilities that a loader can use. In the specific case of Linux, it appears that this field isn't ever used.

That, however, doesn't change the fact that the field still exists, nor that the OP asked about why it exists. Simply saying "it's not used", while true in itself, would/does not answer the question he asked.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • 1
    You may well be right, but I personally don't find this argument entirely convincing (not the performance bit, but the assertion that the loader actually needs to know the sizes of undefined global symbols). – NPE Nov 29 '12 at 15:18
  • Your assumption about Windows is wrong. Windows doesn't map individual functions, it maps segments. And the extra overhead of loading the segment lengths is zero, since it will map the entire segment containing `DllMain()` when a DLL is loaded. – MSalters Nov 29 '12 at 15:22
  • Assume that I prefer independency over performance (and would like to choose - if I could (setting options etc.)). I always thought that I could just change a library with a fixed (thus minor) version _without_ recompiling the _using_ library? So when that is possible, the function's length information in the _using_ lib would be outdated anyway, wouldn't it? – Ingmar Nov 29 '12 at 15:32
  • @MSalters: yes, it works in segments not functions -- same basic idea, just grouping some functions together. As for zero overhead: sorry, but not really true. I'm pretty sure you know Windows well enough for me to simply say "/delayload", and you can pretty much figure the rest out from there. – Jerry Coffin Nov 29 '12 at 15:36
  • 1
    @minastaros: At a guess, the length information is treated as a hint -- given the length information, it can generate the memory map faster. If it finds out later that it's wrong, it can allocate a different piece of address space for the function. – Jerry Coffin Nov 29 '12 at 15:38
  • This answer is *totally* wrong. I am *astounded* that it's currently got 8 up-votes. The loader doesn't know or care about any functions. It operates on *segments*, and `mmap`s the entire PT_LOAD segment that the static linker told it about. The static linker doesn't care about function sizes either, it just packs together `.text` *sections*. – Employed Russian Dec 02 '12 at 16:45
  • @EmployedRussian: The problem here seems to be with your reading comprehension, particularly where I specifically pointed out "intended to accomplish", rather than anything like "what it really does." If there's any thing astounding here it's that you downvote and comment when you apparently haven't read what's actually there. – Jerry Coffin Dec 02 '12 at 18:23
  • @Jerry: what I've concluded from the `readelf` output on my (newer) desktop system is: for symbols from the _own_ lib, the field is >0 (I can imagine some performance benefit here--when used), for global symbols from _other_ libs (the "problem childs") it is =0, except inline methods (>0 as well). That does make sense and matches the expectation I had before. Whatever, important is that I can apply the patch (see my own answer) without breaking anything. During this investigation, I've learned some really useful insights, so thank you (all) very much for your efforts to help me. – Ingmar Dec 02 '12 at 22:52