From b1439396067ca7a1dcfd7edf837a90c85906a346 Mon Sep 17 00:00:00 2001 From: Kevin Lynx Date: Mon, 6 Apr 2015 18:33:29 +0800 Subject: [PATCH] u --- .../2014-11-04-linux-dynamic-library.md | 286 ++++++++++++++ source/_posts/2014-12-03-gdb_stl.md | 368 ++++++++++++++++++ 2 files changed, 654 insertions(+) create mode 100644 source/_posts/2014-11-04-linux-dynamic-library.md create mode 100644 source/_posts/2014-12-03-gdb_stl.md diff --git a/source/_posts/2014-11-04-linux-dynamic-library.md b/source/_posts/2014-11-04-linux-dynamic-library.md new file mode 100644 index 0000000..7e7d317 --- /dev/null +++ b/source/_posts/2014-11-04-linux-dynamic-library.md @@ -0,0 +1,286 @@ +--- +layout: post +title: "linux动态库的种种要点" +category: c/c++ +tags: dynamic library +comments: true +--- + +linux下使用动态库,基本用起来还是很容易。但如果我们的程序中大量使用动态库来实现各种框架/插件,那么就会遇到一些坑,掌握这些坑才有利于程序更稳健地运行。 + +本篇先谈谈动态库符号方面的问题。 + +测试代码可以在[github上找到](https://github.com/kevinlynx/test/tree/master/dytest) + +## 符号查找 + +一个应用程序`test`会链接一个动态库`libdy.so`,如果一个符号,例如函数`callfn`定义于libdy.so中,test要使用该函数,简单地声明即可: + +{% highlight c++ %} +// dy.cpp libdy.so +void callfn() { + ... +} + +// main.cpp test +extern void callfn(); + +callfn(); +{% endhighlight %} + +在链接test的时候,链接器会统一进行检查。 + +同样,在libdy.so中有相同的规则,它可以使用一个外部的符号,**在它被链接/载入进一个可执行程序时才会进行符号存在与否的检查**。这个符号甚至可以定义在test中,形成一种双向依赖,或定义在其他动态库中: + +{% highlight c++ %} +// dy.cpp libdy.so +extern void mfunc(); + +mfunc(); + +// main.cpp test +void mfunc() { + ... +} +{% endhighlight %} + +在生成libdy.so时`mfunc`可以找不到,此时`mfunc`为未定义: + + $ nm libdy.so | grep mfun + U _Z5mfuncv + +但在libdy.so被链接进test时则会进行检查,试着把`mfunc`函数的定义去掉,就会得到一个链接错误: + + ./libdy.so: undefined reference to `mfunc()' + +同样,如果我们动态载入libdy.so,此时当然可以链接通过,但是在载入时同样得到找不到符号的错误: + +{% highlight c++ %} +#ifdef DY_LOAD + void *dp = dlopen("./libdy.so", RTLD_LAZY); + typedef void (*callfn)(); + callfn f = (callfn) dlsym(dp, "callfn"); + f(); + dlclose(dp); +#else + callfn(); +#endif +{% endhighlight %} + +得到错误: + + ./test: symbol lookup error: ./libdy.so: undefined symbol: _Z5mfuncv + +**结论:**基于以上,我们知道,如果一个动态库依赖了一些外部符号,这些外部符号可以位于其他动态库甚至应用程序中。我们可以再链接这个动态库的时候就把依赖的其他库也链接上,或者推迟到链接应用程序时再链接。而动态加载的库,则要保证在加载该库时,进程中加载的其他动态库里已经存在该符号。 + +例如,通过`LD_PRELOAD`环境变量可以让一个进程先加载指定的动态库,上面那个动态加载启动失败的例子,可以通过预先加载包含`mfunc`符号的动态库解决: + + $ LD_PRELOAD=libmfun.so ./test + ... + +但是如果这个符号存在于可执行程序中则不行: + + $ nm test | grep mfunc + 0000000000400a00 T _Z5mfuncv + $ nm test | grep mfunc + 0000000000400a00 T _Z5mfuncv + $ ./test + ... + ./test: symbol lookup error: ./libdy.so: undefined symbol: _Z5mfuncv + + +## 符号覆盖 + +前面主要讲的是符号缺少的情况,如果同一个符号存在多分,则更能引发问题。这里谈到的符号都是全局符号,一个进程中某个全局符号始终是全局唯一的。为了保证这一点,在链接或动态载入动态库时,就会出现忽略重复符号的情况。 + +*这里就不提同一个链接单位(如可执行程序、动态库)里符号重复的问题了* + +### 函数 + +当动态库和libdy.so可执行程序test中包含同名的函数时会怎样?根据是否动态加载情况还有所不同。 + +当直接链接动态库时,libdy.so和test都会链接包含`func`函数的fun.o,为了区分,我把`func`按照条件编译得到不同的版本: + +{% highlight c++ %} +// fun.cpp +#ifdef V2 +extern "C" void func() { + printf("func v2\n"); +} +#else +extern "C" void func() { + printf("func v1\n"); +} +#endif + +// Makefile +test: libdy obj.o mainfn + g++ -g -Wall -c fun.cpp -o fun.o # 编译为fun.o + g++ -g -Wall -c main.cpp #-DDY_LOAD + g++ -g -Wall -o test main.o obj.o fun.o -ldl mfun.o -ldy -L. + +libdy: obj + g++ -Wall -fPIC -c fun.cpp -DV2 -o fun-dy.o # 定义V2宏,编译为fun-dy.o + g++ -Wall -fPIC -shared -o libdy.so dy.cpp -g obj.o fun-dy.o +{% endhighlight %} + +这样,test中的`func`就会输出`func v1`;libdy.so中的`func`就会输出`func v2`。test和libdy.o确实都有`func`符号: + + $ nm libdy.so | grep func + 0000000000000a60 T func + + $nm test | grep func + 0000000000400a80 T func + +在test和libdy.so中都会调用`func`函数: + +{% highlight c++ %} +// main.cpp test +int main(int argc, char **argv) { + func(); + ... + callfn(); // 调用libdy.so中的函数 + ... +} + +// dy.cpp libdy.so +extern "C" void callfn() { + ... + printf("callfn\n"); + func(); + ... +} +{% endhighlight %} + +运行后发现,都**调用的是同一个`func`**: + + $ ./test + ... + func v1 + ... + callfn + func v1 + +**结论**,直接链接动态库时,整个程序运行的时候符号会发生覆盖,只有一个符号被使用。**在实践中**,如果程序和链接的动态库都依赖了一个静态库,而后他们链接的这个静态库版本不同,则很有可能因为符号发生了覆盖而导致问题。(静态库同普通的.o性质一样,参考[浅析静态库链接原理](http://codemacro.com/2014/09/15/inside-static-library/)) + +更复杂的情况中,多个动态库和程序都有相同的符号,情况也是一样,会发生符号覆盖。如果程序里没有这个符号,而多个动态库里有相同的符号,也会覆盖。 + +但是对于动态载入的情况则不同,同样的libdy.so我们在test中不链接,而是动态载入: + +{% highlight c++ %} +int main(int argc, char **argv) { + func(); +#ifdef DY_LOAD + void *dp = dlopen("./libdy.so", RTLD_LAZY); + typedef void (*callfn)(); + callfn f = (callfn) dlsym(dp, "callfn"); + f(); + func(); + dlclose(dp); +#else + callfn(); +#endif + return 0; +} +{% endhighlight %} + +运行得到: + + $ ./test + func v1 + ... + callfn + func v2 + func v1 + +都正确地调用到各自链接的`func`。 + +**结论**,实践中,动态载入的动态库一般会作为插件使用,那么其同程序链接不同版本的静态库(相同符号不同实现),是没有问题的。 + + +### 变量 + +变量本质上也是符号(symbol),但其处理规则和函数还有点不一样(*是不是有点想吐槽了*)。 + +{% highlight c++ %} +// object.h +class Object { +public: + Object() { +#ifdef DF + s = malloc(32); + printf("s addr %p\n", s); +#endif + printf("ctor %p\n", this); + } + + ~Object() { + printf("dtor %p\n", this); +#ifdef DF + printf("s addr %p\n", s); + free(s); +#endif + } + + void *s; +}; + +extern Object g_obj; +{% endhighlight %} + + +我们的程序test和动态库libdy.so都会链接object.o。首先测试test链接libdy.so,test和libdy.so中都会有`g_obj`这个符号: + + // B g_obj 表示g_obj位于BSS段,未初始化段 + + $ nm test | grep g_obj + 0000000000400a14 t _GLOBAL__I_g_obj + 00000000006012c8 B g_obj + $ nm libdy.so | grep g_obj + 000000000000097c t _GLOBAL__I_g_obj + 0000000000200f30 B g_obj + +运行: + + $ ./test + ctor 0x6012c8 + ctor 0x6012c8 + ... + dtor 0x6012c8 + dtor 0x6012c8 + +**`g_obj`被构造了两次,但地址一样**。全局变量只有一个实例,似乎在情理之中。 + +动态载入libdy.so,变量地址还是相同的: + + $ ./test + ctor 0x6012a8 + ... + ctor 0x6012a8 + ... + dtor 0x6012a8 + dtor 0x6012a8 + +**结论**,不同于函数,全局变量符号重复时,不论动态库是动态载入还是直接链接,变量始终只有一个。 + +但诡异的情况是,对象被构造和析构了两次。构造两次倒无所谓,浪费点空间,但是析构两次就有问题。因为析构时都操作的是同一个对象,那么如果这个对象内部有分配的内存,那就会对这块内存造成double free,因为指针相同。打开`DF`宏实验下: + + $ ./test + s addr 0x20de010 + ctor 0x6012b8 + s addr 0x20de040 + ctor 0x6012b8 + ... + dtor 0x6012b8 + s addr 0x20de040 + dtor 0x6012b8 + s addr 0x20de040 + +因为析构的两次都是同一个对象,所以其成员`s`指向的内存被释放了两次,从而产生了double free,让程序coredump了。 + +**总结**,全局变量符号重复时,始终会只使用一个,并且会被初始化/释放两次,是一种较危险的情况,应当避免在使用动态库的过程中使用全局变量。 + + +*完* + + diff --git a/source/_posts/2014-12-03-gdb_stl.md b/source/_posts/2014-12-03-gdb_stl.md new file mode 100644 index 0000000..270f2b1 --- /dev/null +++ b/source/_posts/2014-12-03-gdb_stl.md @@ -0,0 +1,368 @@ +--- +layout: post +title: "基于内存查看STL常用容器内容" +category: c/c++ +tags: stl +comments: true +--- + +有时候在线上使用gdb调试程序core问题时,可能没有符号文件,拿到的仅是一个内存地址,如果这个指向的是一个STL对象,那么如何查看这个对象的内容呢? + +只需要知道STL各个容器的数据结构实现,就可以查看其内容。本文描述了SGI STL实现中常用容器的数据结构,以及如何在gdb中查看其内容。 + +## string + +string,即`basic_string` `bits/basic_string.h`: + +{% highlight c++ %} + mutable _Alloc_hider _M_dataplus; + ... + const _CharT* + c_str() const + { return _M_data(); } + ... + _CharT* + _M_data() const + { return _M_dataplus._M_p; } + + ... + struct _Alloc_hider : _Alloc + { + _Alloc_hider(_CharT* __dat, const _Alloc& __a) + : _Alloc(__a), _M_p(__dat) { } + + _CharT* _M_p; // The actual data. + }; + + size_type + length() const + { return _M_rep()->_M_length; } + + _Rep* + _M_rep() const + { return &((reinterpret_cast<_Rep*> (_M_data()))[-1]); } + + ... + struct _Rep_base + { + size_type _M_length; + size_type _M_capacity; + _Atomic_word _M_refcount; + }; + + struct _Rep : _Rep_base +{% endhighlight %} + +即,string内有一个指针,指向实际的字符串位置,这个位置前面有一个`_Rep`结构,其内保存了字符串的长度、可用内存以及引用计数。当我们拿到一个string对象的地址时,可以通过以下代码获取相关值: + +{% highlight c++ %} + void ds_str_i(void *p) { + char **raw = (char**)p; + char *s = *raw; + size_t len = *(size_t*)(s - sizeof(size_t) * 3); + printf("str: %s (%zd)\n", s, len); + } + + size_t ds_str() { + std::string s = "hello"; + ds_str_i(&s); + return s.size(); + } +{% endhighlight %} + +在gdb中拿到一个string的地址时,可以以下打印出该字符串及长度: + + (gdb) x/1a p + 0x7fffffffe3a0: 0x606028 + (gdb) p (char*)0x606028 + $2 = 0x606028 "hello" + (gdb) x/1dg 0x606028-24 + 0x606010: 5 + +## vector + +众所周知vector实现就是一块连续的内存,`bits/stl_vector.h`。 + +{% highlight c++ %} + template > + class vector : protected _Vector_base<_Tp, _Alloc> + + ... + template + struct _Vector_base + { + typedef typename _Alloc::template rebind<_Tp>::other _Tp_alloc_type; + + struct _Vector_impl + : public _Tp_alloc_type + { + _Tp* _M_start; + _Tp* _M_finish; + _Tp* _M_end_of_storage; + _Vector_impl(_Tp_alloc_type const& __a) + : _Tp_alloc_type(__a), _M_start(0), _M_finish(0), _M_end_of_storage(0) + { } + }; + + + _Vector_impl _M_impl; +{% endhighlight %} + + +可以看出`sizeof(vector)=24`,其内也就是3个指针,`_M_start`指向首元素地址,`_M_finish`指向最后一个节点+1,`_M_end_of_storage`是可用空间最后的位置。 + +{% highlight c++ %} + iterator + end() + { return iterator (this->_M_impl._M_finish); } + const_iterator + ... + begin() const + { return const_iterator (this->_M_impl._M_start); } + ... + size_type + capacity() const + { return size_type(const_iterator(this->_M_impl._M_end_of_storage) + - begin()); } +{% endhighlight %} + +可以通过代码从一个vector对象地址输出其信息: + +{% highlight c++ %} + template + void ds_vec_i(void *p) { + T *start = *(T**)p; + T *finish = *(T**)((char*)p + sizeof(void*)); + T *end_storage = *(T**)((char*)p + 2 * sizeof(void*)); + printf("vec size: %ld, avaiable size: %ld\n", finish - start, end_storage - start); + } + + size_t ds_vec() { + std::vector vec; + vec.push_back(0x11); + vec.push_back(0x22); + vec.push_back(0x33); + ds_vec_i(&vec); + return vec.size(); + } +{% endhighlight %} + +使用gdb输出一个vector中的内容: + + (gdb) p p + $3 = (void *) 0x7fffffffe380 + (gdb) x/1a p + 0x7fffffffe380: 0x606080 + (gdb) x/3xw 0x606080 + 0x606080: 0x00000011 0x00000022 0x00000033 + + +## list + +众所周知list被实现为一个链表。准确来说是一个双向链表。list本身是一个特殊节点,其代表end,其指向的下一个元素才是list真正的第一个节点: + +`bits/stl_list.h` + +{% highlight c++ %} + bool + empty() const + { return this->_M_impl._M_node._M_next == &this->_M_impl._M_node; } + + const_iterator + begin() const + { return const_iterator(this->_M_impl._M_node._M_next); } + + iterator + end() + { return iterator(&this->_M_impl._M_node); } + + ... + + struct _List_node_base + { + _List_node_base* _M_next; ///< Self-explanatory + _List_node_base* _M_prev; ///< Self-explanatory + ... + }; + + template + struct _List_node : public _List_node_base + { + _Tp _M_data; ///< User's data. + }; + + template + class _List_base + { + ... + struct _List_impl + : public _Node_alloc_type + { + _List_node_base _M_node; + ... + }; + + _List_impl _M_impl; + + + template > + class list : protected _List_base<_Tp, _Alloc> +{% endhighlight %} + +所以`sizeof(list)=16`,两个指针。每一个真正的节点首先是包含两个指针,然后是元素内容(`_List_node`)。 + +通过代码输出list的内容: + +{% highlight c++ %} + #define NEXT(ptr, T) do { \ + void *n = *(char**)ptr; \ + T val = *(T*)((char**)ptr + 2); \ + printf("list item %p val: 0x%x\n", ptr, val); \ + ptr = n; \ + } while (0) + + template + void ds_list_i(void *p) { + void *ptr = *(char**)p; + + NEXT(ptr, T); + NEXT(ptr, T); + NEXT(ptr, T); + } + + size_t ds_list() { + std::list lst; + lst.push_back(0x11); + lst.push_back(0x22); + lst.push_back(0x33); + ds_list_i(&lst); + return lst.size(); + } +{% endhighlight %} + +在gdb中可以以下方式遍历该list: + + (gdb) p p + $4 = (void *) 0x7fffffffe390 + (gdb) x/1a p + 0x7fffffffe390: 0x606080 + (gdb) x/1xw 0x606080+16 # 元素1 + 0x606090: 0x00000011 + (gdb) x/1a 0x606080 + 0x606080: 0x6060a0 + (gdb) x/1xw 0x6060a0+16 # 元素2 + 0x6060b0: 0x00000022 + +## map + +map使用的是红黑树实现,实际使用的是`stl_tree.h`实现: + +`bits/stl_map.h` + +{% highlight c++ %} + typedef _Rb_tree, + key_compare, _Pair_alloc_type> _Rep_type; + ... + _Rep_type _M_t; + ... + + iterator + begin() + { return _M_t.begin(); } +{% endhighlight %} + +`bits/stl_tree.h` + +{% highlight c++ %} + struct _Rb_tree_node_base + { + typedef _Rb_tree_node_base* _Base_ptr; + typedef const _Rb_tree_node_base* _Const_Base_ptr; + + _Rb_tree_color _M_color; + _Base_ptr _M_parent; + _Base_ptr _M_left; + _Base_ptr _M_right; + + ... + }; + + template + struct _Rb_tree_node : public _Rb_tree_node_base + { + typedef _Rb_tree_node<_Val>* _Link_type; + _Val _M_value_field; + }; + + + template::__value> + struct _Rb_tree_impl : public _Node_allocator + { + _Key_compare _M_key_compare; + _Rb_tree_node_base _M_header; + size_type _M_node_count; // Keeps track of size of tree. + ... + } + + _Rb_tree_impl<_Compare> _M_impl; + ... + + iterator + begin() + { + return iterator(static_cast<_Link_type> + (this->_M_impl._M_header._M_left)); + } +{% endhighlight %} + +所以可以看出,大部分时候(取决于`_M_key_compare`) `sizeof(map)=48`,主要的元素是: + +{% highlight c++ %} + _Rb_tree_color _M_color; // 节点颜色 + _Base_ptr _M_parent; // 父节点 + _Base_ptr _M_left; // 左节点 + _Base_ptr _M_right; // 右节点 + _Val _M_value_field // 同list中节点技巧一致,后面是实际的元素 +{% endhighlight %} + +同list中的实现一致,map本身作为一个节点,其不是一个存储数据的节点, + +`_Rb_tree::end` + +{% highlight c++ %} + iterator + end() + { return iterator(static_cast<_Link_type>(&this->_M_impl._M_header)); } +{% endhighlight %} + +由于节点值在`_Rb_tree_node_base`后,所以任意时候拿到节点就可以偏移这个结构体拿到节点值,节点的值是一个pair,包含了key和value。 + +在gdb中打印以下map的内容: + +{% highlight c++ %} + size_t ds_map() { + std::map imap; + imap["abc"] = 0xbbb; + return imap.size(); + } +{% endhighlight %} + + (gdb) p/x &imap + $7 = 0x7fffffffe370 + (gdb) x/1a (char*)&imap+24 # _M_left 真正的节点 + 0x7fffffffe388: 0x606040 + (gdb) x/1xw 0x606040+32+8 # 偏移32字节是节点值的地址,再偏移8则是value的地址 + 0x606068: 0x00000bbb + (gdb) p *(char**)(0x606040+32) # 偏移32字节是string的地址 + $8 = 0x606028 "abc" + +或者很多时候没有必要这么装逼+蛋疼: + + (gdb) p *(char**)(imap._M_t._M_impl._M_header._M_left+1) + $9 = 0x606028 "abc" + (gdb) x/1xw (char*)(imap._M_t._M_impl._M_header._M_left+1)+8 + 0x606068: 0x00000bbb + +*完* +