C++在运行状态下修改替换函数逻辑

调试代码时非常期望有改动后直接生效的函数,保持当前的环境 不用程序终止运行 重新编译
这让人自然联想起脚本函数,运行时解析 这点特征使得其 在做到这点上游刃有余
但今天要说的不是用脚本来实现这点,虽然过程有点曲折但纯C++代码一样也能做到这点,原理是利用动态库可以动态加载的特性,来完成部分编译


介绍一下要用到的几个东西
1 系统API void * dlopen( const char * pathname, int mode)  打开一个动态链接库  并返回一个句柄给调用进程。可以使用dlclose()来卸载打开的库
2 系统API void*dlsym(void* handle,const char* symbol) dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址
 注意此符号(symbol)为经过编译器修饰后的函数签名,非C(如C++)函数都不会和原始的代码文件中的命名相同, 想避免麻烦就用extern "C" 来定义动态库函数吧
3 C keyword __attribute__((weak)) 弱符号标记  ,告诉编译器此标记的符号可能被外部模块定义的强符号给覆盖
4 C keyword __attribute__((weakref)) 弱引用标记  ,告诉编译器如果在链接时未找到此符号不需要链接报错,默认赋个0值由运行时决定
5 编译选项 -rdynamic 允许动态库通过 extern 引入执行程序的符号
6 编译选项 -ld  dlopen dlsym 等依赖的库


有了这些东西后讲讲思路吧
假如有一个需要调试的test函数,需要运行时修改
则把这个函数放在动态库 so1 中定义 extern "C" { void test() {...}}
在执行程序代码中定义一个函数指针 typedef void *pf(void); pf ptest = NULL; 定下测试桩 ... if(ptest)*ptest(); ...
在执行程序代码中提供控制台指令负责调用 dlopen 和 dlsym  加载制定库中的函数给ptest赋值
修改代码后只要给重新编译动态库到so2 重新执行 dlopen + dlsym 加载新库的代码覆盖指针的步骤就行

------------------------------------------------------------------------------------------------------
改进一下思路,用函数指针不好看,而且还要dlsym 上去万一函数名字敲错了咋办,而且C++ 函数名被编译器修饰以后如俄罗斯名一样难记 “_ZN2gg4testEv”
在执行程序代码中 申明 __attribute__((weakref)) void test(); 定下测试桩 ... if(test) test(); ...
在动态库  中定义  __attribute__((weak)) void test() {...}

修改代码后只要给重新 dlopen 新库 就行, 看 ..连 extern "C" 和 dlsym 都省了


下面转帖普及下dlopen 和 dlsym 时会遇到的问题及解决方法
------------------------------------------------------------------------------------------------------

一.讨论APP + dlopen一个so的情况
1.APP中的全局变量symbol是否导出

测试一:ldtest1】
main.cpp】:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <ctype.h>
  4. #include <unistd.h>
  5. #include <dlfcn.h>
  6. #include <cassert>
  7. #include <errno.h>typedef void (*func1) (void);int abc = -1;int main ( void )
  8. {
  9.   void *testlib;
  10.   func1 testing_call;
  11.   fprintf( stderr, "abc = %d\n", abc );
  12.   //是NOW还是LAZY,对于符号表没有影响
  13.   //if ( ( testlib = dlopen( "./testlib.so", RTLD_NOW | RTLD_GLOBAL ) ) != NULL )
  14.   if ( ( testlib = dlopen( "./testlib.so", RTLD_LAZY | RTLD_GLOBAL ) ) != NULL )
  15.   {
  16.     testing_call = (func1 )dlsym( testlib, "testing" );
  17.     assert ( testing_call );    ( *testing_call )();
  18.     fprintf( stderr, "abc = %d\n", abc );
  19.   }
  20.   else
  21.   {
  22. fprintf( stderr, "dlopen error:  %s\n", dlerror() );
  23.   }
  24.   return( 0 );
  25. }
复制代码

testlib.cpp】:
  1. extern int abc;extern "C" {
  2. void testing ( void )
  3. {
  4.   abc = 0;
  5.   return;}
  6. }
复制代码

Makefile】:
  1. t = test testlib.so
  2. #这两个选项是一个意思
  3. #LDMODULE = -Wl,-E
  4. LDMODULE = -rdynamicall: $t
  5. clean:
  6. rm -f $t coretestlib.so: testlib.cpp
  7.   g++ -I. -shared -g -o $@ testlib.cpptest: main.cpp
  8.   g++ -I. -L. ${LDMODULE}  main.cpp -g -o $@ -ldl
复制代码

如果未加-Wl,-E 选项,则dlopen打开时抱怨symbol abc找不着,打开失败。

2.APP中的函数名是否导出
测试二:ldtest2
main.cpp】:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <ctype.h>
  4. #include <unistd.h>
  5. #include <dlfcn.h>
  6. #include <cassert>
  7. #include <errno.h>typedef void (*func1) (void);int abc = -1;void func()
  8. {
  9.   printf("hi, func in main\n");
  10. }int main ( void )
  11. {
  12.   void *testlib;
  13.   func1 testing_call;
  14.   fprintf( stderr, "abc = %d\n", abc );
  15.   //if ( ( testlib = dlopen( "./testlib.so", RTLD_NOW | RTLD_GLOBAL ) ) != NULL )
  16.   if ( ( testlib = dlopen( "./testlib.so", RTLD_LAZY | RTLD_GLOBAL ) ) != NULL )
  17.   {
  18.     testing_call = (func1 )dlsym( testlib, "testing" );
  19.     assert ( testing_call );    ( *testing_call )();
  20.     fprintf( stderr, "abc = %d\n", abc );
  21.   }
  22.   else
  23.   {
  24. fprintf( stderr, "dlopen error:  %s\n", dlerror() );
  25.   }
  26.   return( 0 );
  27. }
复制代码

testlib.cpp】:
  1. extern int abc;void func()
  2. {
  3.   printf("hi, func in so\n");
  4. }extern "C" {
  5. void testing ( void )
  6. {
  7.   func();
  8.   abc = 0;
  9.   return;}
  10. }
复制代码

Makefile】:
  1. t = test testlib.so
  2. LDMODULE = -rdynamic
  3. SHARE = -Wl,-Bsymbolicall: $t
  4. clean:
  5. rm -f $t coretestlib.so: testlib.cpp
  6.   g++ -I. -shared ${SHARE} -g -o $@ testlib.cpptest: main.cpp
  7.   g++ -I. -L. ${LDMODULE}  main.cpp -g -o $@ -ldl
复制代码

如果编译主程序是带有rdynamic选项,则symbol func确实导出,具体表现就是testing函数中调用main.cpp中的func(),那么,如何告诉编译器调用testlib.cpp中的func呢,在编译so时加上-Wl,-Bsymbolic选项,表示绑定自己的symbol。

3.如果链接上一个动态库,APP中的函数名是否导出
如果不带rdynamic选项,symbol func不导出,所以如果testlib.cpp中没有定义func(),则上面的ldtest2报错:
dlopen error:  ./testlib.so: undefined symbol: _Z4funcv。
testlib.cpp】:
  1. #include <stdio.h>extern void func();extern "C" {
  2. void testing ( void )
  3. {
  4.   func();
  5.   return;
  6. }
  7. }
复制代码

参考一下LD手册:
--export-dynamic
           When creating a dynamically linked executable, add all  symbols  to the  dynamic symbol table.  The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time. If you do not use this option, the dynamic symbol table  will  normally contain  only  those  symbols  which  are referenced by some dynamic object mentioned in the link.
           如果你没有用这个选项,那么动态符号表里面将只包含动态链接对象引用过的符号,如果没有其他的动态链接对象,则什么也不包含。

上面的例子中,编译主程序时并未链接任何动态库,所以是后一种情况,下面我们作一个试验,人为链上一个库:
【测试:ldtest22】
main.cpp】:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <ctype.h>
  4. #include <unistd.h>
  5. #include <dlfcn.h>
  6. #include <cassert>
  7. #include <errno.h>typedef void (*func1) (void);int abc = -1;void func()
  8. {
  9.   printf("hi, func in main\n");
  10. }int main ( void )
  11. {
  12.   void *testlib;
  13.   func1 testing_call;
  14.   fprintf( stderr, "abc = %d\n", abc );
  15.   //if ( ( testlib = dlopen( "./testlib.so", RTLD_NOW | RTLD_GLOBAL ) ) != NULL )
  16.   if ( ( testlib = dlopen( "./testlib.so", RTLD_LAZY | RTLD_GLOBAL ) ) != NULL )
  17.   {
  18.     testing_call = (func1 )dlsym( testlib, "testing" );
  19.     assert ( testing_call );    ( *testing_call )();
  20.     fprintf( stderr, "abc = %d\n", abc );
  21.   }
  22.   else
  23.   {
  24. fprintf( stderr, "dlopen error:  %s\n", dlerror() );
  25.   }
  26.   return( 0 );
  27. }
复制代码

testlib.cpp】:
  1. #include <stdio.h>extern void func();extern "C" {
  2. void testing ( void )
  3. {
  4.   func();
  5.   return;
  6. }
  7. }
复制代码

sharelib.h】:
#include <stdio.h>
void share();

sharelib.cpp】:
  1. #include "sharelib.h"extern void func();void share()
  2. {
  3.   printf("share:\n");
  4.   //此处如果调用了func,则对应的符号将从执行文件中导出;
  5.   //否则,即便前面声明了extern func(),也不导出符号来
  6.   func();
  7. }
复制代码

Makefile】:
  1. t = test testlib.so libsharelib.soall: $t
  2. clean:
  3.   rm -f $t corelibsharelib.so: sharelib.cpp
  4.   g++ -I. -L. -shared -g -o $@ sharelib.cpp
  5. testlib.so: testlib.cpp
  6.   g++ -I. -shared ${SHARE} -g -o $@ testlib.cpptest: main.cpp libsharelib.so
  7.   g++ -I. -L. ${LDMODULE}  main.cpp -g -o $@ -ldl -lsharelib
复制代码

此处编译主程序并未带有rdynamic选项,但symbol func依然导出,主要原因就在于它动态链上一个共享库libsharelib.so,而在该库中引用了func()这个符号,使得其自动被导出。
          
二.讨论APP link一个so的情况
1.APP中的全局变量symbol是否导出
【测试三:ldtest3】
main.cpp】:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <ctype.h>
  4. #include <unistd.h>
  5. #include <dlfcn.h>
  6. #include <cassert>
  7. #include <errno.h>
  8. #include "testlib.h"int abc = -1;int main ( void )
  9. {
  10.   fprintf( stderr, "abc = %d\n", abc );
  11.   testing();
  12.   fprintf( stderr, "dlopen error:  %s\n", dlerror() );
  13.   return( 0 );
  14. }
复制代码

testlib.h】:
void testing();

testlib.cpp】:
  1. #include "testlib.h"
  2. extern int abc;void testing ( void )
  3. {
  4.   abc = 0;
  5.   return;}
复制代码

Makefile】:
  1. t = test libtestlib.soall: $t
  2. clean:
  3. rm -f $t corelibtestlib.so: testlib.cpp
  4.   g++ -I. -shared -g -o $@ testlib.cpptest: main.cpp libtestlib.so
  5.   g++ -I. -L. ${LDMODULE}  main.cpp -g -o $@ -ldl -ltestlib
复制代码

运行时设置LD_LIBRARY_PATH为当前路径即可
我们并未设置任何特殊编译选项,可见采用-ltestlib的方式,APP中的符号表对so是可见的。

2.APP中的函数名是否导出
测试四:ldtest4
main.cpp】:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <ctype.h>
  4. #include <unistd.h>
  5. #include <dlfcn.h>
  6. #include <cassert>
  7. #include <errno.h>int abc = -1;void func()
  8. {
  9.   printf("hi, func in main\n");
  10. }int main ( void )
  11. {
  12.   fprintf( stderr, "abc = %d\n", abc );
  13.   testing();
  14.   fprintf( stderr, "abc = %d\n", abc );
  15.   return( 0 );
  16. }
复制代码

testlib.h】:
void testing();

testlib.cpp】:
  1. #include "testlib.h"
  2. extern int abc;void func()
  3. {
  4.   printf("hi, func in so\n");
  5. }void testing ( void )
  6. {
  7.   func();
  8.   abc = 0;
  9.   return;}
复制代码

Makefile】:
  1. t = test libtestlib.so
  2. SHARE = -Wl,-Bsymbolicall: $t
  3. clean:
  4. rm -f $t corelibtestlib.so: testlib.cpp
  5.   g++ -I. -shared ${SHARE} -g -o $@ testlib.cpptest: main.cpp libtestlib.so
  6.   g++ -I. -L. ${LDMODULE}  main.cpp -g -o $@ -ldl -ltestlib
复制代码

symbol func确实导出,具体表现就是testing函数中调用main.cpp中的func(),那么,如何告诉编译器调用testlib.cpp中的func呢,在编译so时加上-Wl,-Bsymbolic选项,表示绑定自己的symbol。

三.LD选项说明
在前面测试中,我们谈到了诸如Wl,-E和-Wl,-Bsymbolic之类的选项,这里作一个统一说明:
Wl  表示后面的选项将被作为参数传给连接器LD
-E  等价于--export-dynamic,表示将执行程序中的符号表全部导出
-Bsymbolic  当创建动态库时,如果某一个符号在全局符号表中已经存在,强制采用本地的;而缺省情况下,本地定义被覆盖。这一点,对于保证多个动态库之间互不影响是很重要的,起码,它保证了本地定义的优先性,不在程序员不知情的时候,偷偷用外部定义换掉。此外,如果想保证某一个动态库不影响其他的动态库,则可以采用后面谈到的version-script选项;
--version-script  通过version脚本来控制动态库中的符号是global,还是local的;

我们修改一下ldtest2:
Makefile】:
  1. t = test testlib.so
  2. #LDMODULE = -Wl,-E
  3. LDMODULE = -rdynamic
  4. SHARE = -Wl,-Bsymbolic -Wl,--version-script,versionall: $t
  5. clean:
  6.   rm -f $t coretestlib.so: testlib.cpp
  7.   g++ -I. -shared ${SHARE} -g -o $@ testlib.cpptest: main.cpp
  8.   g++ -I. -L. ${LDMODULE}  main.cpp -g -o $@ -ldl
复制代码

version】:
VERS {
global:
  testing;
local: *;
};

我们同时采用了Bsymbolic和--version-script选项,在version script中指明,只有testing这一个符号将被加入到动态符号表中,这样,就能够保证dlsym可以找到testing,并且load该动态库不增添新的无用符号,因为前面local设的是通配符"*"。

四.几个相关命令
nm -C -D libtest.so
把libtest.so中的所有动态符号用demagle的形式打印出来

objdump -T libtest.so
同上,不过更详细一些
其中,-D选项特别有用,针对version-script选项,只打印global标志的symbol。

猜你喜欢

转载自blog.csdn.net/evilswords/article/details/16864677