Ali Bahrami |
Tuesday Oct 21, 2008
GNU Hash ELF Sections The ELF object format is used by several different operating systems, all sharing a common basic design, but each sporting their own extensions. One of the nice aspects of ELF's design is that it facilitates this, defining a common core, as well as reserving space for each implementation to define its own additions. Those of us in the ELF community try to stay current with each other, as a source of new ideas and inspiration, in order to avoid reinventing the wheel, and out of curiosity. Recently, the GNU linker developers added a new style of hash section to their objects, and I've been learning about them in fine detail. Having done the work, it only makes sense to write it down and share it. This posting describes the layout and interpretation of GNU ELF hash sections. The GNU hash section provides a new hash section for ELF objects, with better performance than the original SYSV hash. The information here was gathered from the following sources:
I did not look at the GNU binutils source code to gather this information. If you spot an error, send me email (First.Last@Sun.COM, where First and Last are replaced with my name) and I'll fix it. Hash FunctionThe GNU hash function uses the DJB (Daniel J Bernstein) hash, which Professor Bernstein originally posted to the comp.lang.c usenet newsgroup years ago:
If you search for this algorithm online, you will find that the
hash expression
is frequently coded ash = h * 33 + c These are equivalent statements, replacing integer multiplication with a presumably cheaper shift and add operation. Whether this is actually cheaper depends on the CPU used. There used to be a significant difference with older machines, but integer multiplication on modern machines is very fast.h = ((h << 5) + h) + c Another variation of this algorithm clips the returned value to 31-bits: However, GNU hash uses the full unsigned 32-bit result.return h & 0x7fffffff; The GNU binutils implementation utilizes the uint_fast32_t type for computing the hash. This type is defined to be the fastest available integer machine type capable of representing at least 32-bits on the current system. As it might be implemented using a wider type, the result is explicitly clipped to a 32-bit unsigned value before being returned.
Dynamic Section RequirementsGNU hash sections place some additional sorting requirements on the contents of the dynamic symbol table. This is in contrast to standard SVR4 hash sections, which allow the symbols to be placed in any order allowed by the ELF standard.A standard SVR4 hash table includes all of the symbols in the dynamic symbol table. However, some of these symbols will never be looked up via the hash table:
With GNU hash, the dynamic symbol table is divided into two parts. The first part receives the symbols that can be omitted from the hash table. GNU hash does not impose any specific order for the symbols in this part of the dynamic symbol table. The second half of the dynamic symbol table receives the symbols that are accessible from the hash table. These symbols are required to be sorted by increasing (hash % nbuckets) value, using the GNU hash function described above. The number of hash buckets (nbuckets) is recorded in the GNU hash section, described below. As a result, symbols which will be found in a single hash chain are adjacent in memory, leading to better cache performance. GNU_HASH sectionA GNU_HASH section consists of four separate parts, in order:The header, hash buckets, and hash chains are always 32-bit words, while the Bloom filter words can be 32 or 64-bit depending on the class of object. This means that ELFCLASS32 GNU_HASH sections consist of only 32-bit words, and therefore have their section header sh_entsize field set to 4. ELFCLASS64 GNU_HASH sections have mixed element size, and therefore set sh_entsize to 0. Assuming that the hash section is aligned properly for accessing ELFCLASS sized words, the (4) 32-bit words directly before the Bloom filter ensure that the filter mask words are always aligned properly and can be accessed directly in memory. Bloom FilterGNU hash sections include a Bloom filter. Bloom filters are probabilistic, meaning that false positives are possible, but false negatives are not. This filter is used to rapidly reject symbol names that will not be found in the object, avoiding the more expensive hash lookup operation. Normally, only one object in a process has the given symbol. Skipping the hash operation for all the other objects can greatly speed symbol lookup.The filter consists of maskwords words, each of which is 32-bits (ELFCLASS32) or 64-bits (ELFCLASS64) depending on the class of object. In the following discussion, C will be used to stand for the size of one mask word in bits. Collectively, the mask words make up a logical bitmask of (C * maskwords) bits. GNU hash uses a k=2 Bloom filter, which means that two independent hash functions are used for each symbol. The Bloom filter reference contains the following statement: The requirement of designing k different independent hash functions can be prohibitive for large k. For a good hash function with a wide output, there should be little if any correlation between different bit-fields of such a hash, so this type of hash can be used to generate multiple "different" hash functions by slicing its output into multiple bit fields.The hash function used by the GNU hash has this property. This fact is leveraged to produce both hash functions required by the Bloom filter from the single hash function described above: As discussed above, the link editor determines how many mask words to use (maskwords) and the amount by which the first hash result is right shifted to produce the second (shift2). The more mask words used, the larger the hash section, but the lower the rate of false positives. I was told in private email that the GNU linker primarily derives shift2 from the base 2 log of the number of symbols entered into the hash table (dynsymcount - symndx), with a minimum value of 5 for ELFCLASS32, and 6 for ELFCLASS64. These values are explicitly recorded in the hash section in order to give the link editor the flexibility to change them in the future should better heuristics emerge.H1 = dl_new_hash(name); H2 = H1 >> shift2; The Bloom filter mask sets one bit for each of the two hash values. Based on the Bloom filter reference, the word containing each bit, and the bit to set would be calculated as: To populate the bits when building the filter:N1 = ((H1 / C) % maskwords); N2 = ((H2 / C) % maskwords); B1 = H1 % C; B2 = H2 % C; and to later test the filter:bloom[N1] |= (1 << B1); bloom[N2] |= (1 << B2); The GNU hash deviates from the above in a significant way. Rather than calculate N1 and N2 separately, a single mask word is used, corresponding to N1 above. This is a conscious decision by the GNU hash developers to optimize cache behavior:(bloom[N1] & (1 << B1)) && (bloom[N2] & (1 << B2)) This makes the 2 hash functions for the Bloom filter more dependent than when two different Ns were used, but in our measurements still has very good ratio of rejecting lookups that should be rejected, and is much more cache friendly. It is very important that we touch as few cache lines during lookup as possible. Therefore, in the GNU hash, the single mask word is actually calculated as: The two bits set in the Bloom filter mask word N are:N = ((H1 / C) % maskwords); The link-editor sets these bits asBITMASK = (1 << (H1 % C)) | (1 << (H2 % C)); And the test used by the runtime linker is:bloom[N] |= BITMASK; (bloom[N1] & BITMASK) == BITMASK; Bit Fiddling: Why maskwords Is Required To Be A Power Of TwoIn general, a Bloom filter can be constructed using an arbitrary number of words. However, as noted above, the GNU hash calls for maskwords to be a power of 2. This requirement allows the modulo operationto instead be written as a simple mask operation:N = ((H1 / C) % maskwords); Note that (maskwords - 1) can be precomputed onceN = ((H1 / C) & (maskwords - 1)); and then used for every hash:MASKWORDS_BITMASK = maskwords - 1; N = ((H1 / C) & MASKWORDS_BITMASK); Bloom Filter Special CasesBloom filters have a pair of interesting special cases:
Hash BucketsFollowing the Bloom filter are nbuckets 32-bit words. Each word N in the array contains the lowest index into the dynamic symbol table for which:Since the dynamic symbol table is sorted by the same key (hash % nbuckets), dynsym[buckets[N]] is the first symbol in the hash chain that will contain the desired symbol if it exists.(dl_new_hash(symname) % nbuckets) == N A bucket element will contain the index 0 if there is no symbol in the hash table for the given value of N. As index 0 of the dynsym is a reserved value, this index cannot occur for a valid symbol, and is therefore non-ambiguous. Hash ValuesThe final part of a GNU hash section contains (dynsymcount - symndx) 32-bit words, one entry for each symbol in the second part of the dynamic symbol table. The top 31 bits of each word contains the top 31 bits of the corresponding symbol's hash value. The least significant bit is used as a stopper bit. It is set to 1 when a symbol is the last symbol in a given hash chain:lsb = (N == dynsymcount - 1) || ((dl_new_hash (name[N]) % nbuckets) != (dl_new_hash (name[N + 1]) % nbuckets)) hashval = (dl_new_hash(name) & ~1) | lsb; Symbol Lookup Using GNU HashThe following shows how a symbol might be looked up in an object using the GNU hash section. We will assume the existence of an in memory record containing the information needed:
To simplify matters, we elide the details of handling
different ELF classes.
In the above, Word is a 32-bit unsigned value, BloomWord is
either 32 or 64-bit depending in the ELFCLASS, and Sym is
either Elf32_Sym or Elf64_Sym.
Given a variable containing the above information for an object, the following pseudo code returns a pointer to the desired symbol if it exists in the object, and NULL otherwise.
Posted at 03:01PM Oct 21, 2008 by ali in Sun | Comments[1] |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Interesting stuff. It's really good to see more toolchain blogs knocking about these days. These things are a mystery to many.
Check out:
http://www.airs.com/blog/
http://nickclifton.livejournal.com/
Posted by John Levon on October 21, 2008 at 06:24 PM MDT #