| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #ifndef SRC_KALLSYMS_KERNEL_SYMBOL_MAP_H_ |
| #define SRC_KALLSYMS_KERNEL_SYMBOL_MAP_H_ |
| |
| #include <stdint.h> |
| |
| #include <array> |
| #include <forward_list> |
| #include <string> |
| #include <vector> |
| |
| namespace perfetto { |
| |
| namespace base { |
| class StringView; |
| } |
| |
| // A parser and memory-efficient container for /proc/kallsyms. |
| // It can store a full kernel symbol table in ~1.2MB of memory and perform fast |
| // lookups using logarithmic binary searches + bounded linear scans. |
| // |
| // /proc/kallsyms is a ~10 MB text file that contains the map of kernel symbols, |
| // as follows: |
| // ffffff8f77682f8c t el0_sync_invalid |
| // ffffff8f77683060 t el0_irq_invalid |
| // ... |
| // In a typipcal Android kernel, it consists of 213K lines. Out of these, only |
| // 116K are interesting for the sake of symbolizing kernel functions, the rest |
| // are .rodata (variables), weak or other useless symbols. |
| // Still, even keeping around 116K pointers would require 116K * 8 ~= 1 MB of |
| // memory, without accounting for any strings for the symbols names. |
| // The SUM(str.len) for the 116K symbol names adds up to 2.7 MB (without |
| // counting their addresses). |
| // However consider the following: |
| // - Symbol addresses are mostly contiguous. Modulo the initial KASLR loading |
| // address, most symbols are few hundreds bytes apart from each other. |
| // - Symbol names are made of tokens that are quite frequent (token: the result |
| // of name.split('_')). If we tokenize the 2.7 MB of strings, the resulting |
| // SUM(distinct_token.len) goes down 2.7MB -> 146 KB. This is because tokens |
| // like "get", "set" or "event" show up thousands of times. |
| // - Symbol names are ASCII strings using only 7 out of 8 bits. |
| // |
| // In the light of this, the in-memory architecture of this data structure is |
| // as follows: |
| // We keep two tables around: (1) a token table and (2) a symbol table. Both |
| // table are a flat byte vector with some sparse lookaside index to make lookups |
| // faster and avoid full linear scans. |
| // |
| // Token table |
| // ----------- |
| // The token table is a flat char buffer. Tokens are variable size (>0). Each |
| // token is identified by its ordinality, so token id 3 is the 3rd token in |
| // the table. All tokens are concatenated together. |
| // Given the ASCII encoding, the MSB is used as a terminator. So instead of |
| // wasting an extra NUL byte for each string, the last char of each token has |
| // the MSB set. |
| // Furthermore, a lookaside index stores the offset of tokens (i.e. Token N |
| // starts at offset O in the buffer) to allow fast lookups. In order to avoid |
| // wasting too much memory, the index is sparse and track the offsets of only |
| // one every kTokenIndexSamplinig tokens. |
| // When looking up a token ID N, the table seeks at the offset of the closest |
| // token <= N, and then scans linearly the next (at most kTokenIndexSamplinig) |
| // tokens, counting the MSBs found, until the right token id is found. |
| // buf: set*get*kernel*load*fpsimd*return*wrapper*el0*skip*sync*neon*bit*aes |
| // ^ ^ ^ |
| // | | | |
| // index: 0@0 4@15 8@21 |
| |
| // Symbol table |
| // ------------ |
| // The symbol table is a flat char buffer that stores for each symbol: its |
| // address + the list of token indexes in the token table. The main caveats are |
| // that: |
| // - Symbol addresses are delta encoded (delta from prev symbol's addr). |
| // - Both delta addresses and token indexes are var-int encoded. |
| // - The LSB of token indexes is used as EOF marker (i.e. the next varint is |
| // the delta-addr for the next symbol). This time the LSB is used because of |
| // the varint encoding. |
| // At parsing time symbols are ordered by address and tokens are sorted by |
| // frequency, so that the top used 64 tokens can be represented with 1 byte. |
| // (Rationale for 64: 1 byte = 8 bits. The MSB bit of each byte is used for the |
| // varint encoding, the LSB bit of each number is used as end-of-tokens marker. |
| // There are 6 bits left -> 64 indexes can be represented using one byte). |
| // In summary the symbol table looks as follows: |
| // |
| // Base address: 0xbeef0000 |
| // Symbol buffer: |
| // 0 1|0 4|0 6|1 // 0xbeef0000: 1,4,6 -> get_fpsimd_wrapper |
| // 8 7|0 3|1 // 0xbeef0008: 7,3 -> el0_load |
| // ... |
| // Like in the case of the token table, a lookaside index keeps track of the |
| // offset of one every kSymIndexSamplinig addresses. |
| // The Lookup(ADDR) function operates as follows: |
| // 1. Performs a logarithmic binary search in the symbols index, finding the |
| // offset of the closest addres <= ADDR. |
| // 2. Skip over at most kSymIndexSamplinig until the symbol is found. |
| // 3. For each token index, lookup the corresponding token string and |
| // concatenate them to build the symbol name. |
| |
| class KernelSymbolMap { |
| public: |
| // The two constants below are changeable only for the benchmark use. |
| // Trades off size of the root |index_| vs worst-case linear scans size. |
| // A higher number makes the index more sparse. |
| static size_t kSymIndexSampling; |
| |
| // Trades off size of the TokenTable |index_| vs worst-case linear scans size. |
| static size_t kTokenIndexSampling; |
| |
| // Parses a kallsyms file. Returns the number of valid symbols decoded. |
| size_t Parse(const std::string& kallsyms_path); |
| |
| // Looks up the closest symbol (i.e. the one with the highest address <= |
| // |addr|) from its absolute 64-bit address. |
| // Returns an empty string if the symbol is not found (which can happen only |
| // if the passed |addr| is < min(addr)). |
| std::string Lookup(uint64_t addr); |
| |
| // Returns the numberr of valid symbols decoded. |
| size_t num_syms() const { return num_syms_; } |
| |
| // Returns the size in bytes used by the adddress table (without counting |
| // the tokens). |
| size_t addr_bytes() const { return buf_.size() + index_.size() * 8; } |
| |
| // Returns the total memory usage in bytes. |
| size_t size_bytes() const { return addr_bytes() + tokens_.size_bytes(); } |
| |
| // Token table. |
| class TokenTable { |
| public: |
| using TokenId = uint32_t; |
| TokenTable(); |
| ~TokenTable(); |
| TokenId Add(const std::string&); |
| base::StringView Lookup(TokenId); |
| size_t size_bytes() const { return buf_.size() + index_.size() * 4; } |
| |
| void shrink_to_fit() { |
| buf_.shrink_to_fit(); |
| index_.shrink_to_fit(); |
| } |
| |
| private: |
| TokenId num_tokens_ = 0; |
| |
| std::vector<char> buf_; // Token buffer. |
| |
| // The value i-th in the vector contains the offset (within |buf_|) of the |
| // (i * kTokenIndexSamplinig)-th token. |
| std::vector<uint32_t> index_; |
| }; |
| |
| private: |
| TokenTable tokens_; // Token table. |
| |
| uint64_t base_addr_ = 0; // Address of the first symbol (after sorting). |
| size_t num_syms_ = 0; // Number of valid symbols stored. |
| std::vector<uint8_t> buf_; // Symbol buffer. |
| |
| // The key is (address - base_addr_), the value is the byte offset in |buf_| |
| // where the symbol entry starts (i.e. the start of the varint that tells the |
| // delta from the previous symbol). |
| std::vector<std::pair<uint32_t /*rel_addr*/, uint32_t /*offset*/>> index_; |
| }; |
| |
| } // namespace perfetto |
| |
| #endif // SRC_KALLSYMS_KERNEL_SYMBOL_MAP_H_ |