標籤:

GCC 下 C++ 中 new int[] 內存的額外信息在哪裡?

條件是gcc下,我有做搜索,看到據說在內存塊前面或後面存放長度信息。於是去做實驗,死活沒找到這個額外信息。

如果使用類,new A[4],則似乎可以看到一點信息,但數據並非簡單size。

如果使用placement方式,則原本類情況下出現的信息也沒有了。

哪位可以講解一下為什麼嗎?或者哪裡可以看到gcc下new,delete的實現源碼或文檔?謝謝!


嗯題主想像的「額外信息」不是在operator new[] (size_t sz)里記錄的,而是編譯器在外面看情況額外生成了代碼。

原因很簡單:全局的operator new (size_t sz)和operator new[] (size_t sz)並不知道要給什麼類型的對象分配空間,所以自然也不知道它們所需要記錄的額外元數據要些什麼。

而這些信息從調用者的一側是知道的,所以就由編譯器在調用者的一側生成代碼來做這些事情。

===================================================================

得把回答順序調整一下免得誤導了讀者?

先看倆例子。現在在我的老MacBook Air 2011上,用的編譯器是llvm-gcc-4.2.1,平台是Mac OS X 10.7.5 x86-64。

第一個例子沒有虛析構函數:

#include &
using namespace std;

class A {
int x, y;
};
// sizeof(A) == 8, x: 4, y: 4

int main() {
A* as = new A[10];
delete [] as;
return 0;
}

這個版本里main()被該版本的GCC編譯為類似下面偽代碼:

int main() {
void* _tmp = ::operator new[](sizeof(A) * 10);
A* as = reinterpret_cast&(_tmp);
::operator delete[](as);
return 0;
}

沒有留下任何C++層面上的「額外信息」。

第二個例子則使用虛析構函數:

#include &
using namespace std;

class A {
int x, y;
public:
~A() { }
};
// sizeof(A) == 16, vptr: 8, x: 4, y: 4
// sizeof(size_t) == 8

int main() {
A* as = new A[10];
delete [] as;
return 0;
}

這個版本的main()則被這個GCC編譯為類似下面偽代碼:

int main() {
void* _tmp = ::operator new[](sizeof(A) * 10 + sizeof(size_t))
A* _tmpA = reinterpret_cast&(
reinterpret_cast&(_tmp) + 1);
{
// array length recorded at offset: -sizeof(size_t)
*(reinterpret_cast&(_tmpA) - 1) = 10;
A* _cur = _tmpA;
for (int _i = 9; _i != -1; _cur++, i--) {
A::A(_cur); // call ctor: initialize vptr, etc
}
}
A* as = _tmpA;

if (as != nullptr) {
size_t _len = *(reinterpret_cast&(as) - 1);
A* _cur = as + _len;
// call dtor from last to first element in the array
while (_cur != as) {
_cur = _cur - 1;
// call virtual dtor through vtable slot 0
auto _vptr = reinterpret_cast&(_cur)[0];
_vptr[0](_cur);
}
// call operator delete[](size_t sz) with the original location
::operator delete[](
reinterpret_cast&(as) - 1);
}
return 0;
}

這個版本里,在new[]時GCC額外給數組分配了1個size_t大小的隱藏欄位來記錄數組元素的個數,並且在new[]之後會按順序調用構造函數;而在delete[]之前逆序會循環調用析構函數,然後再調用全局的operator delete[] (size_t sz)。

在GCC中,貫穿於C++的operator new[] (size_t sz)的實現代碼中的「關鍵點」是「 VEC_NEW_EXPR」。用這個詞搜索GCC的代碼就可以找到關鍵的實現代碼。

例如說,重點之一在這裡:

gcc/init.c at 374fac5db1e3c3a2622705ae097f8685af34d1a4 · gcc-mirror/gcc · GitHub

/* Generate code for a new-expression, including calling the "operator
new" function, initializing the object, and, if an exception occurs
during construction, cleaning up. The arguments are as for
build_raw_new_expr. This may change PLACEMENT and INIT. */

static tree
build_new_1 (vec& **placement, tree type, tree nelts,
vec& **init, bool globally_qualified_p,
tsubst_flags_t complain)

其中從2621行開始是global operator new的處理,它會檢測該數組是否需要「cookie」。這個「cookie」就是題主想找的C++語義層面的「額外信息」。

(TYPE_VEC_NEW_USES_COOKIE):

New macro to indicate when vec new must add a header containing the number of elements in the vector; i.e. when the elements need to be destroyed or vec delete wants to know the size.

/* Use a global operator new. */
/* See if a cookie might be required. */
if (!(array_p TYPE_VEC_NEW_USES_COOKIE (elt_type)))

這個宏的定義是:

gcc/cp-tree.h at b594087e1afe41db9d100f08644715702d6cfc1b · gcc-mirror/gcc · GitHub

/* Nonzero if `new NODE[x]" should cause the allocation of extra
storage to indicate how many array elements are in use. */
#define TYPE_VEC_NEW_USES_COOKIE(NODE)
(CLASS_TYPE_P (NODE)
LANG_TYPE_CLASS_CHECK (NODE)-&>vec_new_uses_cookie)

===================================================================

關於operator new[] (size_t sz)的實現,參考某個版本的GCC對應的libstdc++ / libsupc++吧。

全局的operator new[] (size_t sz)在這裡(這個是throw版;nothrow版在對應的http://new_opvnt.cc里):

gcc/new_opv.cc at master · gcc-mirror/gcc · GitHub

_GLIBCXX_WEAK_DEFINITION void*
operator new[] (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
return ::operator new(sz);
}

直接轉交給operator new(size_t sz)了。多狡猾 &>_&<

然後是全局的operator new (size_t sz)

gcc/new_op.cc at master · gcc-mirror/gcc · GitHub

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;

/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;

while (__builtin_expect ((p = malloc (sz)) == 0, false))
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}

return p;
}

正常路徑上本質上就是調用了libc的malloc(size_t sz)而已,額外信息也不是在這裡寫入的。

同理,默認的全局oeprator delete[] (size_t sz)也是直接轉交給operator delete (size_t sz),後者則轉交給libc的free()。

至於說placement new,默認行為是什麼也不做:

gcc/new at master · gcc-mirror/gcc · GitHub

// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
inline void* operator new[](std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }

所以跟存儲相關的「額外信息」——某個指針所指向的動態申請的存儲空間到底有多大——就要看malloc()的實現了。然而底下的malloc()實現是可以變的,例如說可以動態跟jemalloc、tcmalloc鏈接上,也可以用glibc自己的malloc。大家未必會用同一種方式來記錄「額外信息」。

題主可以另外開個問題問malloc()在哪裡記錄「額外信息」?

我用的這個老MacBook Air上的Mac OS X自帶的libc在libSystem里,其中的malloc叫做magazine_malloc。可以參考這裡的說明:Cocoa with Love: A look at how malloc works on the Mac


using namespace std;
static int c ;
struct A {
A(){
i = c ++;
cerr &<&< __FILE__ &<&< ":" &<&< __LINE__ &<&< ": [" &<&< __FUNCTION__&<&< "] " &<&< endl; } ~A(){ cerr &<&< __FILE__ &<&< ":" &<&< __LINE__ &<&< ": [" &<&< __FUNCTION__&<&< "] " &<&< endl; } long long i; }; void bar(A * p) { delete []p; } int main(int argc, char *argv[]) { A * p = new A[3]; bar(p); return 0; }

這是一段例子代碼。編譯之

g++ -fno-inline O3 -ggdb main.c

開始用 gdb 調試

bash$ gdb ~/tmp/a.out
(gdb) b bar
Breakpoint 1 at 0x400ce0: file nn.cc, line 24.
(gdb) run
Starting program: /home/xxx/tmp/a.out
nn.cc:13: [A]
nn.cc:13: [A]
nn.cc:13: [A]

Breakpoint 1, bar (p=0x602018) at nn.cc:24
24 delete []p;
(gdb) disassemble
Dump of assembler code for function bar(A*):
=&> 0x0000000000400c40 &<+0&>: test %rdi,%rdi
0x0000000000400c43 &<+3&>: je 0x400c80 &
0x0000000000400c45 &<+5&>: push %rbp
0x0000000000400c46 &<+6&>: push %rbx
0x0000000000400c47 &<+7&>: mov %rdi,%rbp
0x0000000000400c4a &<+10&>: sub $0x8,%rsp
0x0000000000400c4e &<+14&>: mov -0x8(%rdi),%rax 讀取數組長度到 $rax
0x0000000000400c52 &<+18&>: lea (%rdi,%rax,8),%rbx 讀取末尾指針位置到 $rbx
0x0000000000400c56 &<+22&>: cmp %rbx,%rdi 是否到達尾指針
0x0000000000400c59 &<+25&>: je 0x400c71 & 是,則返回
0x0000000000400c5b &<+27&>: nopl 0x0(%rax,%rax,1)
0x0000000000400c60 &<+32&>: sub $0x8,%rbx 尾指針 -- &<- 0x0000000000400c64 &<+36&>: mov %rbx,%rdi 設置 this 指針 |
0x0000000000400c67 &<+39&>: callq 0x400d00 & 調用析構函數 |
0x0000000000400c6c &<+44&>: cmp %rbx,%rbp 是否到頭指針了 |
0x0000000000400c6f &<+47&>: jne 0x400c60 & 否,則 ------
0x0000000000400c71 &<+49&>: add $0x8,%rsp
0x0000000000400c75 &<+53&>: lea -0x8(%rbp),%rdi
0x0000000000400c79 &<+57&>: pop %rbx
0x0000000000400c7a &<+58&>: pop %rbp
0x0000000000400c7b &<+59&>: jmpq 0x400900 &<_ZdaPv@plt&>
0x0000000000400c80 &<+64&>: repz retq
End of assembler dump.
(gdb) x /10xg $rdi -8
0x602010: 0x0000000000000003 0x0000000000000000
0x602020: 0x0000000000000001 0x0000000000000002
0x602030: 0x0000000000000000 0x0000000000020fd1
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000

我加些注釋

1. "-fno-inline 取消 inline 的優化,讓彙編出來的 `bar` 函數變短一一些

2. "-O3" 優化過的彙編看起來更加容易

3. 寄存器 `$rdi` 存儲的是第一個參數 p,

4. 用 x 命令查看內存,看到 p 前面有一個8位元組表示長度 0x602010 那裡。

5. gcc 是倒著調用析構函數的。就是先析構 p[2], p[1], 最後析構 p[0]

6. linux 下 X86 64 參數的傳遞是用寄存器傳遞的, $rdi, $rsi, $rcx, $rdx, $r9, $r10, windows 下的不是這樣的,沒有實驗過。

總結,數組長度就在 p[-1]那裡,但是我不確定這個是標準,高度懷疑不是標準,是編譯器實現相關。


大學學C++的時候做過簡單的表象研究。

似乎用GCC的話,它把new的數組長度存在了指針之前1個int的位置。

所以猜想實現的方式是在new的時候多申請幾個位元組,用來放數組長度(也許還有更多,用來放單位的size?因為delete[]會逐一調用析構函數,所以不僅需要知道數組的byte length還需要知道單位個數),然後最終返回的時候就偏移一下。


其他不懂,但是gcc源碼可以到gnu官網下載,

http://gcc.gnu.org/

http://ftp.gnu.org/gnu/gcc/

http://ftp.gnu.org/gnu/gcc/gcc-5.1.0/gcc-5.1.0.tar.gz


推薦閱讀:

如何利用C++的特性,去實現C中的可變參?
一個關於visual studio的問題?
2017年6月,GCC 7.1 對於 C++17 標準的支持情況如何了?
關於VS2015的報錯問題,?

TAG:C | GCC | 編譯器 |