Skip to content

Stack trace와 kallsyms

Stack Frame

image
  • frame pointer 레지스터는 linked list 구조처럼 스택에 있는 이전의 frame pointer의 주소를 저장한다. (마지막 프레임은 다음 프레임이 없으므로 0을 저장한다.)

  • stack의 frame pointer 앞에는 LR 레지스터의 값(리턴시 점프할 주소)도 같이 저장된다.

  • 위와 같은 특성을 이용하면, stack frame을 순회하면서 frame pointer와 LR의 값들을 확인할 수 있다. 아래는 이 특성을 이용하여 구현한 단순한 dump_stack 함수이다.

    #include <stdint.h>
    #include <stdio.h>
    void dump_stack()
    {
    uint64_t *fp;
    uint64_t lr;
    fp = __builtin_frame_address(0);
    for (;fp;fp=(uint64_t*)*fp) {
    lr = fp[1];
    printf(" [lr:%016lx fp:%016lx]\n", lr, fp[0]);
    }
    }
    int main(){
    dump_stack();
    }
  • 해당 코드를 ARM64에서 실행하면 아래와 같은 결과를 얻을 수 있다. frame pointer가 0에 도달할 때까지 stack frame을 순회한 것을 알 수 있다.

    Terminal window
    [lr:00000055751977d8 fp:0000007fe9015b60]
    [lr:0000007f9ac2c090 fp:0000007fe9015b70]
    [lr:0000005575197694 fp:0000000000000000]
  • 하지만 주소만 보여주는 것만으로는 정보를 파악하기 쉽지 않다. 디버깅을 위해선 주소에 대응하는 함수 이름도 같이 출력을 해줘야 한다.

KALLSYMS

  • 리눅스에서는 커널의 심볼 정보들을 담당하는 kallsym으로 함수 이름(심볼)을 가져올 수 있다.
  • /proc/kallsyms 파일을 열면 현재 커널의 여러 심볼 정보들을 살펴볼 수 있다.

Kallsyms에서 정보를 읽어오는 과정 (/scripts/kallsyms.c)

  • /scripts/kallsyms.c라는 스크립트를 사용해 vmlinux의 심볼 정보들을 편하게 읽을 수 있다.

  • 해당 스크립트는 별도로 컴파일 되어 실행 가능한 파일이다. 입력으로는 파일의 심볼을 읽는 nm 유틸리티의 stdout을 사용한다.

  • 이 스크립트를 기준으로 kallsyms의 구조를 살펴보자.

  • kallsyms에서 주로 사용되는 구조체는 4개가 있다.

    • sym_entry: 하나의 심볼에 대응하는 자료 구조이다. 심볼의 주소(addr), 이름(sym[])을 저장한다.

    • addr_range: 어떤 영역에 대응하는 자료 구조 입니다. 영역의 시작과 끝에 대응하는 심볼과 주소를 저정한다.

    • token_profit: 2개의 문자로 이루어진 문자열의 빈도 수를 기록하는 테이블이다. 2개의 문자가 가질 수 있는 조합 수는 0x10000(=256x256) 이므로 테이블의 길이는 0x10000 이다.

    • best_table: 압축에 사용되는 매핑 테이블이다. 각각의 char이 매핑되는 문자열을 저장한다.

      struct sym_entry {
      unsigned long long addr;
      unsigned int len;
      unsigned int start_pos;
      unsigned int percpu_absolute;
      unsigned char sym[];
      };
      struct addr_range {
      const char *start_sym, *end_sym;
      unsigned long long start, end;
      };
      static struct sym_entry **table;
      static unsigned int table_size, table_cnt;
      static int token_profit[0x10000];
      /* the table that holds the result of the compression */
      static unsigned char best_table[256][2];
      static unsigned char best_table_len[256];
      // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L35
  • addr_range는 아래와 같이 총 3개의 영역(text, init text, percpu)을 미리 정의한 뒤 실행된다. 각 영역의 시작과 끝에 대응하는 심볼은 알지만 그 주소는 아직 모르기 때문이다.

    static struct addr_range text_ranges[] = {
    { "_stext", "_etext" },
    { "_sinittext", "_einittext" },
    };
    #define text_range_text (&text_ranges[0])
    #define text_range_inittext (&text_ranges[1])
    static struct addr_range percpu_range = {
    "__per_cpu_start", "__per_cpu_end", -1ULL, 0
    };
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L44
  • 전체 과정은 크게 5단계로 나뉜다.

    int main(int argc, char **argv) {
    if (argc >= 2) {
    int i;
    for (i = 1; i < argc; i++) {
    if(strcmp(argv[i], "--all-symbols") == 0)
    all_symbols = 1;
    else if (strcmp(argv[i], "--absolute-percpu") == 0)
    absolute_percpu = 1;
    else if (strcmp(argv[i], "--base-relative") == 0)
    base_relative = 1;
    else
    usage();
    }
    } else if (argc != 1)
    usage();
    read_map(stdin); // (1)
    shrink_table(); // (2)
    if (absolute_percpu)
    make_percpus_absolute();
    sort_symbols(); // (3)
    if (base_relative)
    record_relative_base();
    optimize_token_table(); // (4)
    write_src(); // (5)
    return 0;
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L810
    1. symbol 파싱
    2. 유효하지 않은 심볼 삭제
    3. symbol entry 정렬
    4. symbol entry 압축
    5. symbol entry 출력

1. symbol 파싱

  • read_map 함수에서는 symbol을 파싱하고 테이블에 추가한다.

    static void read_map(FILE *in)
    {
    struct sym_entry *sym;
    while (!feof(in)) {
    sym = read_symbol(in); // EOF에 도달할 때까지 계속해서 재귀호출한다.
    if (!sym)
    continue;
    sym->start_pos = table_cnt;
    if (table_cnt >= table_size) {
    table_size += 10000;
    table = realloc(table, sizeof(*table) * table_size);
    if (!table) {
    fprintf(stderr, "out of memory\n");
    exit (1);
    }
    }
    table[table_cnt++] = sym;
    }
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L257
  • symbol에 대한 핵심 파싱은 read_symbol 함수를 통해 이뤄진다. 파싱한 데이터는 symbol_entry의 형태로 반환되고, 테이블에 추가된다.

  • 해당 함수는 3가지의 일을 수행한다.

    1. 입력에서 심볼의 주소(addr), 타입(type), 심볼의 이름(name)을 받아온다.
    2. 받아온 정보들을 이용해 symbol_entry를 생성 및 초기화한다.
    3. 읽어온 심볼이 addr_range에 시작과 끝에 해당하는 심볼이라면, 이 심볼의 주소를 addr_range에 저장한다.
    static struct sym_entry *read_symbol(FILE *in)
    {
    char name[500], type;
    unsigned long long addr;
    unsigned int len;
    struct sym_entry *sym;
    int rc;
    // 표준 입출력을 통해 addr, type, name을 받아온다.
    rc = fscanf(in, "%llx %c %499s\n", &addr, &type, name);
    ...
    if (strcmp(name, "_text") == 0)
    _text = addr;
    /* Ignore most absolute/undefined (?) symbols. */
    if (is_ignored_symbol(name, type))
    return NULL;
    // 전역으로 생성한 percpu addr_range와 text addr_range의 시작과 끝에 해당하는 심볼인지 확인 후, 맞다면 주소를 addr_range에 저장한다.
    check_symbol_range(name, addr, text_ranges, ARRAY_SIZE(text_ranges));
    check_symbol_range(name, addr, &percpu_range, 1);
    /* include the type field in the symbol name, so that it gets
    * compressed together */
    // sym_entry, type, name을 저장할 수 있도록 동적 할당을 받는다. sym_entry.sym[0]에 type을 저장하기에 문자열의 크기보다 1 더 큰 사이즈를 요청한다.
    len = strlen(name) + 1;
    sym = malloc(sizeof(*sym) + len + 1);
    if (!sym) {
    fprintf(stderr, "kallsyms failure: "
    "unable to allocate required amount of memory\n");
    exit(EXIT_FAILURE);
    }
    // 생성한 sym_entry에 addr, len, type, name을 저장한다. percpu_absolute는 0으로 초기화한다.
    sym->addr = addr;
    sym->len = len;
    sym->sym[0] = type;
    strcpy(sym_name(sym), name);
    sym->percpu_absolute = 0;
    return sym;
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L124

2. 유효하지 않은 심볼 삭제

  • shrink_table에서는 유효하지 않은 symbol들을 삭제한다.
  • 테이블을 순회하면서 invalid한 심볼들에 대해 free를 해줌으로써 valid한 symbol_entry만 남도록 한다.
/* remove all the invalid symbols from the table */
static void shrink_table(void)
{
unsigned int i, pos;
pos = 0;
for (i = 0; i < table_cnt; i++) {
if (symbol_valid(table[i])) {
if (pos != i)
table[pos] = table[i];
pos++;
} else {
free(table[i]);
}
}
table_cnt = pos;
/* When valid symbol is not registered, exit to error */
if (!table_cnt) {
fprintf(stderr, "No valid symbol.\n");
exit(1);
}
}
// https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L234

3. symbol entry 정렬

  • symbol_entry 테이블 정렬은 sort_symbol 함수에서 실행된다.

  • compare_symbols 함수를 통해 qsort 방식으로 정렬을 수행한다. 정렬 기준은 주소이고, 주소가 동일한 경우 다른 속성을 비교한다.)

    static int compare_symbols(const void *a, const void *b) {
    const struct sym_entry *sa = *(const struct sym_entry **)a;
    const struct sym_entry *sb = *(const struct sym_entry **)b;
    int wa, wb;
    /* sort by address first */
    if (sa->addr > sb->addr)
    return 1;
    if (sa->addr < sb->addr)
    return -1;
    ...
    }
    static void sort_symbols(void) {
    qsort(table, table_cnt, sizeof(table[0]), compare_symbols);
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L739

4. symbol entry 압축

  • symbol entry 압축은 optimize_table 함수에서 이뤄진다.

  • optimize_table 함수는 아래와 같이 총 3개의 단계로 구성되어 있다.

    static void optimize_token_table(void) {
    build_initial_token_table();
    insert_real_symbols_in_table();
    optimize_result();
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L695
  • build_initial_tok_table():

    • 구성한 symbol_entry 테이블을 순회하면서 learn_symbol 함수를 호출한다.

    • learn_symbol 함수는 symbol_entrysym(type+name)과 len을 인자로 받는다.

    • learn_symbol은 받은 문자열 symobl_entry.sym을 순회하면서, char[2]의 분포를 token_profit 테이블에 반영한다.

      /* count all the possible tokens in a symbol */
      static void learn_symbol(const unsigned char *symbol, int len)
      {
      int i;
      for (i = 0; i < len - 1; i++)
      token_profit[ symbol[i] + (symbol[i + 1] << 8) ]++;
      }
      /* decrease the count for all the possible tokens in a symbol */
      static void forget_symbol(const unsigned char *symbol, int len)
      {
      int i;
      for (i = 0; i < len - 1; i++)
      token_profit[ symbol[i] + (symbol[i + 1] << 8) ]--;
      }
      /* do the initial token count */
      static void build_initial_tok_table(void)
      {
      unsigned int i;
      for (i = 0; i < table_cnt; i++)
      learn_symbol(table[i]->sym, table[i]->len);
      }
      // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L554
  • insert_real_symbols_in_table()

    • insert_real_symbols_in_tablesymbol_entry 테이블을 순회하면서 sym에 사용되는 문자를 기록한다.

    • 한 번이라도 사용된 char은 best_table에 기록되고, 사용되지 않은 char은 기록되지 않는다.

      ```c
      /* start by placing the symbols that are actually used on the table */
      static void insert_real_symbols_in_table(void)
      {
      unsigned int i, j, c;
      for (i = 0; i < table_cnt; i++) {
      for (j = 0; j < table[i]->len; j++) {
      c = table[i]->sym[j];
      best_table[c][0]=c;
      best_table_len[c]=1;
      }
      }
      }
      // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L682
      ```
  • optimize_result()

    • optimize_resultbest_table을 순회하면서 사용되지 않은 char을 찾고, 이 char을 빈번하게 사용된 char[2]와 매핑한다.

    • 빈번하게 char[2]는 앞서 구성한 token_profit에서 가장 큰 값을 가진 것에 해당한다.

    • 그런 다음 symbol_entry에 사용된 문자열을 매핑한 문자로 치환하는 작업을 수행한다.

      /* this is the core of the algorithm: calculate the "best" table */
      static void optimize_result(void)
      {
      int i, best;
      /* using the '\0' symbol last allows compress_symbols to use standard
      * fast string functions */
      for (i = 255; i >= 0; i--) {
      /* if this table slot is empty (it is not used by an actual
      * original char code */
      if (!best_table_len[i]) {
      /* find the token with the best profit value */
      best = find_best_token();
      if (token_profit[best] == 0)
      break;
      /* place it in the "best" table */
      best_table_len[i] = 2;
      best_table[i][0] = best & 0xFF;
      best_table[i][1] = (best >> 8) & 0xFF;
      /* replace this token in all the valid symbols */
      compress_symbols(best_table[i], i);
      }
      }
      }
      // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L653
      • compress_symbols 함수에서 핵심 압축 로직을 수행한다. 첫 번째 인자는 압축할 char[2]이고, 두 번째 인자는 매핑된 1byte 정수이다.

      • symbol_entry 테이블을 순회하면서 각 symbol_entry.sym에 압축 대상의 문자열이 존재하는지 확인한다. 만약 그렇다면, 해당 문자열을 idx로 치환한다.

      • 압축한 symbol_entry.sym을 반영하기 위해 이전의 내용을 지우고(forget_symbol), 압축이 완료된 후에는 다시 learn_symbols를 호출하여 token_profit을 최신으로 업데이트한다.

        static void compress_symbols(const unsigned char *str, int idx)
        {
        unsigned int i, len, size;
        unsigned char *p1, *p2;
        for (i = 0; i < table_cnt; i++) {
        len = table[i]->len;
        p1 = table[i]->sym;
        /* find the token on the symbol */
        p2 = find_token(p1, len, str);
        if (!p2) continue;
        /* decrease the counts for this symbol's tokens */
        forget_symbol(table[i]->sym, len);
        size = len;
        do {
        *p2 = idx;
        p2++;
        size -= (p2 - p1);
        memmove(p2, p2 + 1, size);
        p1 = p2;
        len--;
        if (size < 2) break;
        /* find the token on the symbol */
        p2 = find_token(p1, size, str);
        } while (p2);
        table[i]->len = len;
        /* increase the counts for this symbol's new tokens */
        learn_symbol(table[i]->sym, len);
        }
        }

5. symbol entry 출력

  • write_src에서는 assembly 파일 포맷에 맞춰 필요한 정보들을 출력한다.

  • 먼저 Archtiecture(64bit or 32bit)에 따라 매크로(PTR, ALGN)를 정의한다. 심볼 관련 정보들은 .rodata 섹션에 배치된다.

    static void write_src(void)
    {
    unsigned int i, k, off;
    unsigned int best_idx[256];
    unsigned int *markers;
    char buf[KSYM_NAME_LEN];
    printf("#include <asm/bitsperlong.h>\n");
    printf("#if BITS_PER_LONG == 64\n");
    printf("#define PTR .quad\n");
    printf("#define ALGN .balign 8\n");
    printf("#else\n");
    printf("#define PTR .long\n");
    printf("#define ALGN .balign 4\n");
    printf("#endif\n");
    printf("\t.section .rodata, \"a\"\n");
    output_label("kallsyms_addresses");
    ...
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/kallsyms.c#L386
  • 그런 다음 symbol_entry를 순회하면서 각각의 address를 출력한다. symbol_entry의 갯수도 kallsyms_num_syms라는 이름으로 출력한다. 출력의 형식은 옵션에 따라 다르다.

    ...
    for (i = 0; i < table_cnt; i++) {
    printf("\tPTR\t%#llx\n", table[i]->addr);
    }
    printf("\n");
    output_label("kallsyms_num_syms");
    printf("\t.long\t%u\n", table_cnt);
    printf("\n");
    ...
  • 그런 다음 symbol_entry의 sym을 출력한다. symbol_entry.sym은 가변 길이이므로 검색의 용이성을 위해 marker라는 검색 인덱스를 만든다. marker는 256개의 symbol_entry.sym마다 오프셋을 저장한다.

    ...
    /* table of offset markers, that give the offset in the compressed stream
    * every 256 symbols */
    markers = malloc(sizeof(unsigned int) * ((table_cnt + 255) / 256));
    if (!markers) {
    fprintf(stderr, "kallsyms failure: "
    "unable to allocate required memory\n");
    exit(EXIT_FAILURE);
    }
    output_label("kallsyms_names");
    off = 0;
    for (i = 0; i < table_cnt; i++) {
    if ((i & 0xFF) == 0)
    markers[i >> 8] = off;
    ...
    if (table[i]->len <= 0xFF) {
    /* Most symbols use a single byte for the length. */
    printf("\t.byte 0x%02x", table[i]->len);
    off += table[i]->len + 1;
    } else {
    /* "Big" symbols use a zero and then two bytes. */
    printf("\t.byte 0x00, 0x%02x, 0x%02x",
    (table[i]->len >> 8) & 0xFF,
    table[i]->len & 0xFF);
    off += table[i]->len + 3;
    }
    for (k = 0; k < table[i]->len; k++)
    printf(", 0x%02x", table[i]->sym[k]);
    printf("\n");
    }
    printf("\n");
    /* 마커 출력 */
    output_label("kallsyms_markers");
    for (i = 0; i < ((table_cnt + 255) >> 8); i++)
    printf("\t.long\t%u\n", markers[i]);
    printf("\n");
    free(markers);
    ...
  • 최종적으로 0x00에서 0xFF까지 순회하면서 char마다 대응하는 문자열 또는 char를 출력한다. 어떤 char은 재압축이 되었을 수도 있으므로 expand_symbol을 통해 압축을 해제한 문자열을 buf에 저장한다.

  • 이렇게 출력된 정보들은 kallsyms_token_table에서 찾을 수 있다.

    ...
    output_label("kallsyms_token_table");
    off = 0;
    for (i = 0; i < 256; i++) {
    best_idx[i] = off;
    expand_symbol(best_table[i], best_table_len[i], buf);
    printf("\t.asciz\t\"%s\"\n", buf);
    off += strlen(buf) + 1;
    }
    printf("\n");
    output_label("kallsyms_token_index");
    for (i = 0; i < 256; i++)
    printf("\t.short\t%d\n", best_idx[i]);
    printf("\n");
    }
  • 정리하면, write_src는 다음과 같은 여러 정보들을 출력한다.

    • kallsyms_address: 심볼들의 주소
    • kallsyms_num_syms: symbol의 갯수
    • kallsyms_names: symbol들의 압축된 이름
    • kallsyms_marker: kallsyms_names의 검색 인덱스
    • kallsyms_token_table: 압축된 문자(char)가 매핑 된 문자 또는 문자열
  • vmlinux를 생성하는 Makefile command에서는 /scripts/link-vmlinux.sh라는 스크립트가 실행되는데, CONFIG_KALLSYMS 옵션이 활성화 되어 있다면 kallsyms 관련 일을 수행한다.

    vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
    +$(call if_changed_dep,link-vmlinux)
  • link-vmlinux.sh에서 사용되는 핵심 함수는 vmlinux_linkkallsyms이다.

    • vmlinux_link: 첫 번째 인자로 받는 오브젝트 파일과 vmlinux.o를 링크하고, 두 번째 인자에서 받은 이름으로 출력 파일을 저장한다.

    • kallsyms: 첫 번째 인자로 받은 오프젝트 파일의 심볼 정보를 추출하고 어셈블리 파일으로 저장한다. 이때 저장하는 파일의 이름은 함수의 2번째 인자와 같다.

      link-vmlinux.sh
      # https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/link-vmlinux.sh#L148
      kallsymso="" # /script/kallsyms을 통해 생성한 최종 오브젝트 파일 이름
      kallsyms_vmlinux="" # /script/kallsyms에 입력으로 넘겨준 최종 오브젝트 파일의 이름
      if [ -n "${CONFIG_KALLSYMS}" ]; then
      kallsymso=.tmp_kallsyms2.o
      kallsyms_vmlinux=.tmp_vmlinux2
      # (1)
      vmlinux_link "" .tmp_vmlinux1
      kallsyms .tmp_vmlinux1 .tmp_kallsyms1.o
      # (2)
      vmlinux_link .tmp_kallsyms1.o .tmp_vmlinux2
      kallsyms .tmp_vmlinux2 .tmp_kallsyms2.o
      # (3)
      size1=$(${CONFIG_SHELL} "${srctree}/scripts/file-size.sh" .tmp_kallsyms1.o)
      size2=$(${CONFIG_SHELL} "${srctree}/scripts/file-size.sh" .tmp_kallsyms2.o)
      if [ $size1 -ne $size2 ] || [ -n "${KALLSYMS_EXTRA_PASS}" ]; then
      kallsymso=.tmp_kallsyms3.o
      kallsyms_vmlinux=.tmp_vmlinux3
      vmlinux_link .tmp_kallsyms2.o .tmp_vmlinux3
      kallsyms .tmp_vmlinux3 .tmp_kallsyms3.o
      fi
      fi
      info LD vmlinux
      # (4)
      vmlinux_link "${kallsymso}" vmlinux
    1. vmlinux.o를 링크하여 tmp_vmlinux1이라는 임시 오브젝트 파일을 생성한다. 이 임시 오브젝트 파일은 kallsyms의 입력 파일로 제공되며, .tmp_kallsyms1.o라는 중간 산출물 오브젝트 파일을 생성한다. 해당 중간 산출물 파일은 자신에 대한 심볼 정보(kallsyms_token_table, ..)들을 포함하지 않는다.

    2. 앞서 생성한 .tmp_kallsyms1.ovmlinux.o를 링크하여 .tmp_vmlinux2라는 오브젝트 파일을 생성한다. 해당 오브젝트는 이전 .tmp_vmlinux1와 다르게 kallsyms 관련 심볼들에 대한 올바른 정보를 포함하고 있다. 이렇게 생성한 오브젝트 파일을 /script/kallsyms의 입력으로 주어 최종적인 심볼 관련 오브젝트 파일 .tmp_kallsyms2.o를 생성한다.

    3. tmp_kallsyms1.o.tmp_kallsyms2.o의 크기가 다르다면 변환 단계를 추가로 실행한다.

    4. 최종적으로 vmlinux.o와 생성한 최종 오브젝트를 링크하여 vmlinux 파일을 생성한다.

심볼 정보 API kernel/kallsyms.c

  • 해당 파일에서는 생성한 여러 심볼 정보를 사용하는 여러 API를 제공한다.

  • 리눅스 커널에서 사용되는 표준 출력 함수인 printk()의 포인터 관련 추가 기능에도 내부적으로 kallsyms의 API가 사용된다.

  • 핵심 함수로 __sprint_symbol이 있다.

/* Look up a kernel symbol and return it in a text buffer. */
static int __sprint_symbol(char *buffer, unsigned long address,
int symbol_offset, int add_offset, int add_buildid)
{
char *modname;
const unsigned char *buildid;
const char *name;
unsigned long offset, size;
int len;
address += symbol_offset;
name = kallsyms_lookup_buildid(address, &size, &offset, &modname, &buildid,
buffer);
if (!name)
return sprintf(buffer, "0x%lx", address - symbol_offset);
if (name != buffer)
strcpy(buffer, name);
len = strlen(buffer);
offset -= symbol_offset;
if (add_offset)
len += sprintf(buffer + len, "+%#lx/%#lx", offset, size);
if (modname) {
...
}
return len;
}
// https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/kernel/kallsyms.c#L482
  • 주소에 대응하는 정보를 조회하는 kallsyms_lookup_buildid 함수를 살펴보자.

    static const char *kallsyms_lookup_buildid(unsigned long addr,
    unsigned long *symbolsize,
    unsigned long *offset, char **modname,
    const unsigned char **modbuildid, char *namebuf)
    {
    const char *ret;
    namebuf[KSYM_NAME_LEN - 1] = 0;
    namebuf[0] = 0;
    // 입력으로 받은 주소가 커널 영역인지 확인한다. 아니라면 별도의 처리 루틴으로 빠진다.
    if (is_ksym_addr(addr)) {
    unsigned long pos;
    // get_symbol_pos 함수를 통해 주소에 대응하는 심볼의 인덱스를 구한다. 또한 해당 함수의 오프셋과 사이즈도 가져온다.
    pos = get_symbol_pos(addr, symbolsize, offset);
    // 구한 인덱스를 가지고 kallsyms_name에 위치한 압축된 문자열을 구한다. 그런 다음 문자열을 압축 해제한다.
    kallsyms_expand_symbol(get_symbol_offset(pos),
    namebuf, KSYM_NAME_LEN);
    if (modname)
    *modname = NULL;
    if (modbuildid)
    *modbuildid = NULL;
    // 압축 해제한 문자열의 주소를 반환될 변수에 저장한다.
    ret = namebuf;
    goto found;
    }
    /* See if it's in a module or a BPF JITed image. */
    ret = module_address_lookup(addr, symbolsize, offset,
    modname, modbuildid, namebuf);
    if (!ret)
    ret = bpf_address_lookup(addr, symbolsize,
    offset, modname, namebuf);
    if (!ret)
    ret = ftrace_mod_address_lookup(addr, symbolsize,
    offset, modname, namebuf);
    found:
    cleanup_symbol_name(namebuf);
    return ret;
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/kernel/kallsyms.c#L397
  • get_symbol_pos() 함수는 조사하려는 주소가 심볼들의 주소를 저장했던 kallsyms_address라는 테이블에서 몇 번쨰 인덱스에 해당하는지 탐색한다.

  • 주소에 대응하는 인덱스를 구하는 get_symbol_pos 함수는 이진 탐색으로 구현되었다. 앞서 심볼들의 주소를 저장할 때 정렬을 했기 때문에 이진 탐색이 가능하다.

  • 다만, 몇몇 심볼들은 동일한 주소를 가지고 있기에 해당 함수의 크기와 오프셋을 구하기 위해 추가적인 루틴이 존재한다.

    static unsigned long get_symbol_pos(unsigned long addr,
    unsigned long *symbolsize,
    unsigned long *offset)
    {
    unsigned long symbol_start = 0, symbol_end = 0;
    unsigned long i, low, high, mid;
    /* This kernel should never had been booted. */
    if (!IS_ENABLED(CONFIG_KALLSYMS_BASE_RELATIVE))
    BUG_ON(!kallsyms_addresses);
    else
    BUG_ON(!kallsyms_offsets);
    /* Do a binary search on the sorted kallsyms_addresses array. */
    low = 0;
    high = kallsyms_num_syms;
    while (high - low > 1) {
    mid = low + (high - low) / 2;
    if (kallsyms_sym_address(mid) <= addr)
    low = mid;
    else
    high = mid;
    }
    /*
    * Search for the first aliased symbol. Aliased
    * symbols are symbols with the same address.
    */
    while (low && kallsyms_sym_address(low-1) == kallsyms_sym_address(low))
    --low;
    symbol_start = kallsyms_sym_address(low);
    /* Search for next non-aliased symbol. */
    for (i = low + 1; i < kallsyms_num_syms; i++) {
    if (kallsyms_sym_address(i) > symbol_start) {
    symbol_end = kallsyms_sym_address(i);
    break;
    }
    }
    /* If we found no next symbol, we use the end of the section. */
    if (!symbol_end) {
    if (is_kernel_inittext(addr))
    symbol_end = (unsigned long)_einittext;
    else if (IS_ENABLED(CONFIG_KALLSYMS_ALL))
    symbol_end = (unsigned long)_end;
    else
    symbol_end = (unsigned long)_etext;
    }
    if (symbolsize)
    *symbolsize = symbol_end - symbol_start;
    if (offset)
    *offset = addr - symbol_start;
    return low;
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/kernel/kallsyms.c#L321
  • 구한 인덱스로 압축된 문자열의 주소를 구하기 위해 get_symbol_offset() 함수를 사용한다.

    /*
    * Find the offset on the compressed stream given and index in the
    * kallsyms array.
    */
    static unsigned int get_symbol_offset(unsigned long pos)
    {
    const u8 *name;
    int i;
    /*
    * Use the closest marker we have. We have markers every 256 positions,
    * so that should be close enough.
    */
    name = &kallsyms_names[kallsyms_markers[pos >> 8]];
    /*
    * Sequentially scan all the symbols up to the point we're searching
    * for. Every symbol is stored in a [<len>][<len> bytes of data] format,
    * so we just need to add the len to the current pointer for every
    * symbol we wish to skip.
    */
    for (i = 0; i < (pos & 0xFF); i++)
    name = name + (*name) + 1;
    return name - kallsyms_names;
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/kernel/kallsyms.c#L116
  • 다음으로, kallsyms_expand_symbol() 함수에서는 구한 pos에 위치한 압축된 문자열을 압축 해제한다. 만약 조사하는 주소가 모듈, bpf, ftrace에 속한다면 별도의 처리를 수행한다.

  • 압축 해제를 위해선 kallsyms_token_table을 사용한다.

    /*
    * Expand a compressed symbol data into the resulting uncompressed string,
    * if uncompressed string is too long (>= maxlen), it will be truncated,
    * given the offset to where the symbol is in the compressed stream.
    */
    static unsigned int kallsyms_expand_symbol(unsigned int off,
    char *result, size_t maxlen)
    {
    int len, skipped_first = 0;
    const char *tptr;
    const u8 *data;
    /* Get the compressed symbol length from the first symbol byte. */
    data = &kallsyms_names[off];
    len = *data;
    data++;
    /*
    * Update the offset to return the offset for the next symbol on
    * the compressed stream.
    */
    off += len + 1;
    /* If zero, it is a "big" symbol, so a two byte length follows. */
    if (len == 0) {
    len = (data[0] << 8) | data[1];
    data += 2;
    off += len + 2;
    }
    /*
    * For every byte on the compressed symbol data, copy the table
    * entry for that byte.
    */
    while (len) {
    tptr = &kallsyms_token_table[kallsyms_token_index[*data]];
    data++;
    len--;
    while (*tptr) {
    if (skipped_first) {
    if (maxlen <= 1)
    goto tail;
    *result = *tptr;
    result++;
    maxlen--;
    } else
    skipped_first = 1;
    tptr++;
    }
    }
    tail:
    if (maxlen)
    *result = '\0';
    /* Return to offset to the next symbol. */
    return off;
    }
    // https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/kernel/kallsyms.c#L42

참고