Можно ли определить, является ли символ переменной или функцией в C?

Я реализую некоторые ограниченные функции удаленной отладки для приложения, написанного на C, работающего на Linux. Цель состоит в том, чтобы связаться с приложением и найти значение произвольной переменной или запустить произвольную функцию.

Я могу искать символы через dlsym() звонки, но я не могу определить, относится ли возвращаемый адрес к функции или переменной. Есть ли способ определить информацию о наборе с помощью этой таблицы символов?

5 ответов

Решение

Вы можете прочитать файл /proc/self/maps и анализируем первые три поля каждой строки:

<begin-addr>-<end-addr> rwxp ...

Затем вы ищете строку, содержащую адрес, который вы ищете, и проверяете права доступа:

  • r-x: это код;
  • rw-: это записываемые данные;
  • r--: это данные только для чтения;
  • любая другая комбинация: что-то странное (rwxp: сгенерированный код,...).

Например, следующая программа:

#include <stdio.h>

void foo() {}
int x;

int main()
{
    int y;
    printf("%p\n%p\n%p\n", foo, &x, &y);
    scanf("%*s");
    return 0;
}

... в моей системе выдает такой вывод:

0x400570
0x6009e4
0x7fff4c9b4e2c

... и это соответствующие строки из /proc/<pid>/maps:

00400000-00401000 r-xp 00000000 00:1d 641656       /tmp/a.out
00600000-00601000 rw-p 00000000 00:1d 641656       /tmp/a.out
....
7fff4c996000-7fff4c9b7000 rw-p 00000000 00:00 0    [stack]
....

Итак, адреса: код, данные и данные.

На платформах x86 вы можете проверить инструкции, используемые для настройки стека для функции, если вы можете посмотреть в ее адресное пространство. Обычно это:

push ebp
mov ebp, esp

Я не уверен в платформах x64, но думаю, что это похоже:

push rbp
mov rbp, rsp

Это описывает соглашение о вызове C

Имейте в виду, однако, что оптимизация компилятора может оптимизировать эти инструкции. Если вы хотите, чтобы это работало, вам, возможно, придется добавить флаг, чтобы отключить эту оптимизацию. Я полагаю, что для GCC -fno-omit-frame-pointer сработает.

Одним из возможных решений является извлечение таблицы символов для приложения путем анализа вывода утилиты nm. nm содержит информацию о типе символа. Символы с типом T (глобальный текст) являются функциями.

Проблема с этим решением состоит в том, что вы должны убедиться, что ваша таблица символов соответствует цели (особенно, если вы собираетесь использовать ее для извлечения адресов, хотя использование ее в сочетании с dlsym() будет более безопасным). Метод, который я использовал для обеспечения того, чтобы сделать генерацию таблицы символов частью процесса сборки как этап постобработки.

Это может быть сделано путем объединения dlsym() а также dladdr1(),

#define _GNU_SOURCE

#include <dlfcn.h>
#include <link.h>
#include <stdio.h>

int symbolType(void *sym) {
    ElfW(Sym) *pElfSym;
    Dl_info i;

    if (dladdr1(sym, &i, (void **)&pElfSym, RTLD_DL_SYMENT))
        return ELF32_ST_TYPE(pElfSym->st_info);

    return 0;
}

int main(int argc, char *argv[]) {
    for (int i=1; i < argc; ++i) {
        printf("Symbol [%s]: ", argv[i]);

        void *mySym = dlsym(RTLD_DEFAULT, argv[i]);

        // This will not work with symbols that have a 0 value, but that's not going to be very common
        if (!mySym)
            puts("not found!");
        else {
            int type = symbolType(mySym);
            switch (type) {
                case STT_FUNC: puts("Function"); break;
                case STT_OBJECT: puts("Data"); break;
                case STT_COMMON: puts("Common data"); break;
                /* get all the other types from the elf.h header file */
                default: printf("Dunno! [%d]\n", type);
            }
        }
    }

    return 0;
}

Я думаю, это не очень надежный метод, но он может работать:

Возьмите адрес хорошо известной функции, такой как main() и адрес общеизвестной глобальной переменной.

Теперь возьмите адрес неизвестного символа и вычислите абсолютное значение разницы между этим адресом и двумя другими. Наименьшее различие будет означать, что неизвестный адрес ближе к функции или к глобальной переменной, а это означает, что, вероятно, это другая функция или другая глобальная переменная.

Этот метод работает в предположении, что компилятор / компоновщик упакует все глобальные переменные в определенный блок памяти, а все функции - в другой блок памяти. Компилятор Microsoft, например, поместил все глобальные переменные перед (нижние адреса в виртуальной памяти) функциями.

Я предполагаю, что вы не захотите проверять локальные переменные, так как адрес которых не может быть возвращен функцией (после завершения функции локальная переменная теряется)

Другие вопросы по тегам