Переопределить вызов функции в C

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

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

Я создаю новый исходный файл, как это:

#include <apiheader.h>    

const char *getObjectName (object *anObject)
{
    if (anObject == NULL)
        return "(null)";
    else
        return "name should be here";
}

Я компилирую все остальные источники, как обычно, но сначала связываю их с этой функцией, а потом с библиотекой API. Это прекрасно работает, за исключением того, что я не могу явно вызвать реальную функцию внутри моей переопределяющей функции.

Есть ли более простой способ "переопределить" функцию, не получая ссылки / ошибки компиляции / предупреждения? В идеале я хочу иметь возможность переопределить функцию, просто скомпилировав и связав дополнительный файл или два, а не возиться с опциями связывания или изменив фактический исходный код моей программы.

10 ответов

Решение

Если вы хотите захватывать / изменять вызовы только для своего источника, самое простое решение - собрать заголовочный файл (intercept.h) с:

#ifdef INTERCEPT
    #define getObjectName(x) myGetObectName(x)
#endif

и реализовать функцию следующим образом (в intercept.c который не включает intercept.h):

const char *myGetObjectName (object *anObject) {
    if (anObject == NULL)
        return "(null)";
    else
        return getObjectName(anObject);
}

Затем убедитесь, что каждый исходный файл, в который вы хотите перехватить вызов, имеет:

#include "intercept.h"

на вершине.

Затем, когда вы компилируете с "-DINTERCEPTmsgstr "все файлы будут вызывать вашу функцию, а не реальную, и ваша функция все еще может вызывать реальную.

Компиляция без "-DINTERCEPT"предотвратит перехват.

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

С gcc, под Linux вы можете использовать --wrap флаг компоновщика вот так:

gcc program.c -Wl,-wrap,getObjectName -o program

и определите свою функцию как:

const char *__wrap_getObjectName (object *anObject)
{
    if (anObject == NULL)
        return "(null)";
    else
        return __real_getObjectName( anObject ); // call the real function
}

Это гарантирует, что все звонки на getObjectName() перенаправляются к вашей функции оболочки (во время ссылки). Этот очень полезный флаг, однако, отсутствует в gcc под Mac OS X.

Не забудьте объявить функцию-обертку с extern "C" если вы компилируете с g++, хотя.

Вы можете переопределить функцию, используя LD_PRELOAD трюк - посмотри man ld.so, Вы компилируете разделяемую библиотеку с помощью своей функции и запускаете двоичный файл (вам даже не нужно изменять двоичный файл!), Как LD_PRELOAD=mylib.so myprog,

В теле вашей функции (в разделяемой lib) вы пишете так:

const char *getObjectName (object *anObject) {
  static char * (*func)();

  if(!func)
    func = (char *(*)()) dlsym(RTLD_NEXT, "getObjectName");
  printf("Overridden!\n");     
  return(func(anObject));    // call original function
}

Вы можете переопределить любую функцию из общей библиотеки, даже из stdlib, без изменения / перекомпиляции программы, так что вы можете выполнить трюк с программами, для которых у вас нет исходного кода. Разве это не приятно?

Если вы используете GCC, вы можете сделать свою функцию weak, Они могут быть переопределены неслабыми функциями:

test.c:

#include <stdio.h>

__attribute__((weak)) void test(void) { 
    printf("not overridden!\n"); 
}

int main() {
    test();
}

Что оно делает?

$ gcc test.c
$ ./a.out
not overridden!

test1.c:

#include <stdio.h>

void test(void) {
    printf("overridden!\n");
}

Что оно делает?

$ gcc test1.c test.c
$ ./a.out
overridden!

К сожалению, это не будет работать для других компиляторов. Но у вас могут быть слабые объявления, которые содержат переопределяемые функции в своем собственном файле, помещая просто включение в файлы реализации API, если вы компилируете с помощью GCC:

weakdecls.h:

__attribute__((weak)) void test(void);
... other weak function declarations ...

functions.c:

/* for GCC, these will become weak definitions */
#ifdef __GNUC__
#include "weakdecls.h"
#endif

void test(void) { 
    ...
}

... other functions ...

Недостатком этого является то, что он не работает полностью без каких-либо действий с файлами API (требуются эти три строки и слабые decls). Но как только вы сделаете это изменение, функции можно легко переопределить, написав глобальное определение в одном файле и связав его в.

Часто желательно изменить поведение существующих баз кода, оборачивая или заменяя функции. Когда редактирование исходного кода этих функций является жизнеспособным вариантом, это может быть простой процесс. Когда источник функций не может быть отредактирован (например, если функции предоставляются библиотекой системы C), тогда требуются альтернативные методы. Здесь мы представляем такие методы для платформ UNIX, Windows и Macintosh OS X.

Это отличный PDF-файл, рассказывающий о том, как это было сделано в OS X, Linux и Windows.

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

Перехват произвольных функций на платформах Windows, UNIX и Macintosh OS X (2004), Даниэль С. Майерс и Адам Л. Базинет.

Вы можете скачать PDF прямо из другого места (для резервирования).

И, наконец, если два предыдущих источника каким-то образом загорятся, вот результат поиска Google.

Вы можете определить указатель на функцию как глобальную переменную. Синтаксис абонентов не изменится. Когда ваша программа запускается, она может проверить, установлен ли какой-либо флаг командной строки или переменная среды для включения ведения журнала, затем сохранить исходное значение указателя функции и заменить его функцией ведения журнала. Вам не потребуется специальная сборка с включенным ведением журнала. Пользователи могут включить ведение журнала "в поле".

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

foo.h:

typedef const char* (*GetObjectNameFuncPtr)(object *anObject);
extern GetObjectNameFuncPtr GetObjectName;

foo.cpp:

const char* GetObjectName_real(object *anObject)
{
    return "object name";
}

const char* GetObjectName_logging(object *anObject)
{
    if (anObject == null)
        return "(null)";
    else
        return GetObjectName_real(anObject);
}

GetObjectNameFuncPtr GetObjectName = GetObjectName_real;

void main()
{
    GetObjectName(NULL); // calls GetObjectName_real();

    if (isLoggingEnabled)
        GetObjectName = GetObjectName_logging;

    GetObjectName(NULL); // calls GetObjectName_logging();
}

Основываясь на ответе @Johannes Schaub, мы предлагаем решение, подходящее для кода, который вам не принадлежит.

Псевдоним функции, которую вы хотите переопределить для слабо определенной функции, а затем переопределите ее самостоятельно.

override.h

#define foo(x) __attribute__((weak))foo(x)

foo.c

function foo() { return 1234; }

override.c

function foo() { return 5678; }

Используйте специфичные для шаблона значения переменных в вашем Makefile, чтобы добавить флаг компилятора -include override.h,

%foo.o: ALL_CFLAGS += -include override.h

В сторону: Возможно, вы могли бы также использовать -D 'foo(x) __attribute__((weak))foo(x)' определить ваши макросы.

Скомпилируйте и свяжите файл с вашей переопределением (override.c).

  • Это позволяет вам переопределить одну функцию из любого исходного файла без необходимости изменения кода.

  • Недостатком является то, что вы должны использовать отдельный заголовочный файл для каждого файла, который вы хотите переопределить.

Есть также хитрый способ сделать это в компоновщике с использованием двух библиотек-заглушек.

Библиотека #1 связана с библиотекой хоста и предоставляет переопределенный символ под другим именем.

Библиотека № 2 связана с библиотекой № 1, перехватывает вызов и вызывает переопределенную версию в библиотеке № 1.

Будьте очень осторожны с заказами ссылок здесь, иначе это не сработает.

Я попробовал решение @vaughan, и я думаю, что есть что сказать.

main.c

      #include <stdio.h>

void main (void)
{
    func1();

}

original.c : этот файл содержит функцию, которую вы хотите переопределить, и предполагается, что у вас есть только объектный файл.

      #include <stdio.h>

void func1 (void)
{
    printf("in original func1()\n");
}

mock.c : Этот файл содержит вашу реализацию переопределенной функции.

#include <stdio.h>

      void func1 (void)
{
    printf("in mock func1()\n");
}

decl.h:

       void func1 (void); // no weak declaration at all

Makefile1: ссылка на mock. о и оригинал. а

      ALL:
    gcc -c mock.c -o mock.o
    gcc -c original.c -o original.o
    ar cr original.a original.o <============ HERE use archive
    gcc -include override.h main.c mock.o original.a -o main

Makefile2: ссылка на оба файла mock. о и оригинал. о

      ALL:
    gcc -c mock.c -o mock.o
    gcc -c original.c -o original.o  <============= NO archive
    gcc -include override.h main.c mock.o original.o -o main

Makefile3: ссылка на оригинал. а и издеваться. а

      ALL:
    gcc -c mock.c -o mock.o
    gcc -c original.c -o original.o
    ar cr mock.a mock.o
    ar cr original.a original.o
    gcc -include override.h main.c mock.a original.a -o main

С Makefile1 выведите:

      xxx@xxx-host:~/source/override$ make ALL
gcc -c mock.c -o mock.o
gcc -c original.c -o original.o
ar cr original.a original.o
gcc -include override.h main.c mock.o original.a -o main

xxx@xxx-host:~/source/override$ ./main
in mock func1()

С Makefile2 выведите:

      gcc -c mock.c -o mock.o
gcc -c original.c -o original.o
gcc -include override.h main.c mock.o original.o -o main
original.o: In function `func1':
original.c:(.text+0x0): multiple definition of `func1'
mock.o:mock.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
Makefile:2: recipe for target 'ALL' failed
make: *** [ALL] Error 1

С Makefile3 выведите:

      xxx@xxx-host:~/source/override$ make ALL -f Makefile3
gcc -c mock.c -o mock.o
gcc -c original.c -o original.o
ar cr mock.a mock.o
ar cr original.a original.o
gcc -include decl.h main.c mock.a original.a -o main

xxx@xxx-host:~/source/override$ ./main
in mock func1()

Итак, давайте проверим символы:

С помощью Makefile1 и Makefile3 :

      xxx@xxx-host:~/source/override$ nm mock.a

mock.o:
0000000000000000 T func1  <=========== strong symbol
                 U _GLOBAL_OFFSET_TABLE_
                 U puts

xxx@xxx-host:~/source/override$ nm original.a

original.o:
0000000000000000 T func1  <=========== strong symbol
                 U _GLOBAL_OFFSET_TABLE_
                 U puts

Итак, кажется, что критичным моментом является то, что нам не нужно использовать __attribute__((weak)), просто помните, что:

  • функция, определенная в объектном файле, может переопределить архивный файл.
  • функция, определенная в первом архивном файле, может иметь приоритет над последними.

Если изменить decl.h на это:

       __attribute__((weak)) void func1 (void);

Makefile3 потерпит неудачу:

      xxx@xxx-host:~/source/override$ make ALL -f Makefile3
gcc -c mock.c -o mock.o
gcc -c original.c -o original.o
ar cr mock.a mock.o
ar cr original.a original.o
gcc -include decl.h main.c mock.a original.a -o main

xxx@xxx-host:~/source/override$ ./main
Segmentation fault (core dumped)

Далее я попробовал:

Makefile4 : ссылка только на mock.a

      ALL:
    gcc -c mock.c -o mock.o
    ar cr mock.a mock.o
    gcc -include decl.h main.c mock.a -o main

Выход :

      Segmentation fault (core dumped)

Makefile5 : ссылка только на mock.o

      ALL:
    gcc -c mock.c -o mock.o
    ar cr mock.a mock.o
    gcc -include decl.h main.c mock.o -o main

Выход :

      in mock func1()

Дамп символов с nm:

Makefile4 :

      0000000000000824 r __FRAME_END__
                 w func1 <================ func1 is a weak symbol
0000000000200fb8 d _GLOBAL_OFFSET_TABLE_

Makefile5 :

      000000000000085c r __FRAME_END__
0000000000000646 T func1 <================ func1 is a strong symbol and has an address
0000000000200fb8 d _GLOBAL_OFFSET_TABLE_

Полагаю, это связано с неисправностью сегмента. Итак, я разбираю весь двоичный файл (тот, у которого есть ошибка сегмента) и проверяю ту часть, где я не могу найти тело:

      0000000000000520 <func1@plt>:
 520:   ff 25 aa 0a 20 00       jmpq   *0x200aaa(%rip)        # 200fd0 <func1>
 526:   68 00 00 00 00          pushq  $0x0
 52b:   e9 e0 ff ff ff          jmpq   510 <.plt>

и:

      000000000000064a <main>:
 64a:   55                      push   %rbp
 64b:   48 89 e5                mov    %rsp,%rbp
 64e:   e8 cd fe ff ff          callq  520 <func1@plt>
 653:   90                      nop

В то время как для двоичного файла без ошибки сегмента, разборка выглядит так, как показано ниже, где я могу найти func1 тело:

      000000000000063a <main>:
 63a:   55                      push   %rbp
 63b:   48 89 e5                mov    %rsp,%rbp
 63e:   e8 03 00 00 00          callq  646 <func1>

и:

      0000000000000646 <func1>:
 646:   55                      push   %rbp
 647:   48 89 e5                mov    %rsp,%rbp
 64a:   48 8d 3d 93 00 00 00    lea    0x93(%rip),%rdi        # 6e4 <_IO_stdin_used+0x4>
 651:   e8 ba fe ff ff          callq  510 <puts@plt>
 656:   90                      nop
 657:   5d                      pop    %rbp
 658:   c3                      retq   
 659:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

Пока я до сих пор не могу полностью объяснить, почему происходит сбой сегмента. Мне нужно найти больше материалов. Надеюсь, кто-нибудь сможет пролить свет, прежде чем я получу ответ.

Вы также можете использовать разделяемую библиотеку (Unix) или DLL (Windows), чтобы сделать это (это было бы небольшим снижением производительности). Затем вы можете изменить DLL/ так, чтобы она загружалась (одна версия для отладки, одна версия для не-отладки).

Я делал подобное в прошлом (не для достижения того, чего вы пытаетесь достичь, но основная предпосылка та же), и это сработало хорошо.

[Редактировать на основании комментария ОП]

Фактически, одна из причин, по которой я хочу переопределить функции, заключается в том, что я подозреваю, что они ведут себя по-разному в разных операционных системах.

Есть два распространенных способа (которые я знаю), чтобы справиться с этим, совместно используемый способ lib / dll или написание различных реализаций, на которые вы ссылаетесь.

Для обоих решений (совместно используемых библиотек или разных ссылок) у вас будет foo_linux.c, foo_osx.c, foo_win32.c (или лучший способ - linux/foo.c, osx/foo.c и win32/foo.c), а затем скомпилировать и связать с соответствующим.

Если вы ищете как разный код для разных платформ, так и debug -vs- release, я бы, вероятно, склонялся к использованию совместно используемого решения lib/DLL, так как оно наиболее гибкое.

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