2011-04-07 234 views
15

我試圖運行下面的程序,但得到一些奇怪的錯誤:C函數指針鑄造空指針

文件1.C:

typedef unsigned long (*FN_GET_VAL)(void); 

FN_GET_VAL gfnPtr; 

void setCallback(const void *fnPointer) 
{ 
    gfnPtr = *((FN_GET_VAL*) (&fnPointer)); 
} 

文件2.C:

extern FN_GET_VAL gfnPtr; 

unsigned long myfunc(void) 
{ 
    return 0; 
} 

main() 
{ 
    setCallback((void*)myfunc); 
    gfnPtr(); /* Crashing as value was not properly 
       assigned in setCallback function */ 
} 

這裏gfnPtr()在使用gcc編譯時在64位suse linux上崩潰。但它成功地調用了gfnPtr()VC6和SunOS。

但是,如果我改變了下面給出的功能,它工作正常。

void setCallback(const void *fnPointer) 
{ 
    int i; // put any statement here 
    gfnPtr = *((FN_GET_VAL*) (&fnPointer)); 
} 

請幫助解決問題的原因。謝謝。

回答

26

該標準不允許將函數指針轉換爲void*。您只能投射到另一個函數指針類型。 6.3.2.3§8:

一個指針,指向一種類型 的功能可以被轉換爲一個指針到另一個類型的 功能和背面 再次

重要的是,必須投退在使用指針調用函數之前(在技術上,指向兼容類型,在6.2.7中定義「compatible」)。

+0

感謝您的答覆。我會小心不要混合數據和函數指針。但在這種情況下,我無法弄清楚發生這種情況的原因。如果我用-m32(32位)編譯並運行,它工作得很好,但是如果我使用-m64(64位)編譯它會給出問題。另外,如果我添加一個像** int i; **這樣的語句,然後它就可以正常工作了。不知道可能堆棧損壞的原因,但如何檢查。 – Manoj 2011-04-07 11:28:49

9

我有經驗的三個規則的時候纔來的數據指針和代碼指針:

  • 不要混合數據指針和代碼指針
  • 不要混合數據指針和代碼指針
  • 不要有史以來混合數據指針和代碼指針!

以下功能:

void setCallback(const void *fnPointer) 
{ 
    gfnPtr = *((FN_GET_VAL*) (&fnPointer)); 
} 

你有數據指針,你的情況下,以一個函數指針。 (更不用說,你通過首先獲取指針本身的地址來做到這一點,在解除引用之前將它指向指向指針的指針)。

嘗試把它改寫爲:

void setCallback(FN_GET_VAL fnPointer) 
{ 
    gfnPtr = fnPointer; 
} 

此外,您還可以(或應該)下降設置指針當鑄鋼:

main() 
{ 
    setCallback(myfunc); 
    gfnPtr(); 
} 

額外的好處是,你現在可以使用由編譯器執行的正常類型檢查。

+0

感謝您的回覆。我會小心不要混合數據和函數指針。但在這種情況下,我無法弄清楚發生這種情況的原因。如果我用-m32(32位)編譯並運行,它工作得很好,但是如果我使用-m64(64位)編譯它會給出問題。另外,如果我添加一個像** int i; **這樣的語句,然後它就可以正常工作了。不知道可能堆棧損壞的原因,但如何檢查。 – Manoj 2011-04-07 11:25:37

+0

對於分析來說,獲取函數指針的地址與將&賦值給函數名稱(這是一個無操作)不同。 – 2013-05-22 04:43:11

2

我會建議一個可能的部分的解釋。

@Manoj如果您檢查(或可以提供)由兩個編譯器生成的SetCallback的彙編列表,我們可以得到明確的答案。

首先,Pascal Couq的陳述是正確的,Lindydancer展示瞭如何正確設置回調。我的回答只是試圖解釋實際問題。

我認爲問題源於Linux和其他平臺使用不同的64位模型(請參閱64-bit models on Wikipedia)。請注意,Linux使用LP64(int是32位)。我們需要更多關於其他平臺的更多細節。如果它是SPARC64,它使用ILP64(int是64位)。

據我所知,這個問題只能在Linux下觀察到,如果你引入了一個int局部變量,那麼問題就消失了。你有沒有試過這種優化或關閉?最有可能的是,這種黑客攻擊對優化沒有任何好處。

在這兩種64位模型下,無論指向代碼還是數據,指針都應該是64位。然而,這可能不是這種情況(例如分段存儲模型);因此,帕斯卡爾和林德塞瑟的告誡。

如果指針大小相同,剩下的就是可能的堆棧對齊問題。引入一個本地int(在Linux下是32位)可以改變對齊。如果void *和函數指針具有不同的對齊要求,這隻會起作用。可疑的情況。

儘管如此,不同的64位內存模型很可能是您觀察到的原因。歡迎您提供彙編列表,以便我們分析它們。

+0

您鏈接到的維基百科條目是錯誤的。 Sparc上用於Solaris的64位ABI是LP64而不是ILP64。順便說一句SPARC64是富士通獨家Sparc實施的品牌名稱。 – 2016-02-23 12:44:06

12

該標準不幸地不允許在數據指針和函數指針之間進行轉換(因爲這在一些非常模糊的平臺上可能沒有意義),儘管POSIX和其他人需要這種轉換。一種解決方法不是投射指針,而是投射一個指向指針的指針(編譯器可以做到這一點,它可以在所有常規平臺上完成工作)。

typedef void (*FPtr)(void); // Hide the ugliness 
FPtr f = someFunc;   // Function pointer to convert 
void* ptr = *(void**)(&f); // Data pointer 
FPtr f2 = *(FPtr*)(&ptr); // Function pointer restored 
+0

「,因爲這在一些非常晦澀的平臺上可能沒有意義」真正晦澀的平臺,如Linux,Mac OS X,iOS,Microsoft Windows和Android,它們都具有數據執行保護功能。通過數據執行保護,代碼和數據不可互換。注意:POSIX要求強制轉換爲void指針,但它不要求這樣的結果指針可用,除非轉換回函數指針。 – 2013-09-08 02:45:26

+0

@JonathanBaldwin你說的話只有在代碼指針的大小不同於數據指針的平臺上纔有意義。指針只是一個地址,因此永遠不會在可執行區中。但是,地址指向的數據可能位於可執行區域。所以你可能無法讀取指針指向的內存。但是你當然可以使用這個方法將它從函數指針轉換爲void *並從void *轉換爲函數指針。除非平臺對於函數指針的大小不同於數據指針,否則它將起作用。 – rxantos 2014-02-20 08:09:41

+0

@rxantos當然,從數據到函數指針的強制轉換在使用DXP的ia32系統上執行起來並不重要,但它沒有任何意義,因爲這樣的指針無法轉換爲數據指針或使用系統調用而變得不可用在DXP中打孔(即使區域可執行)。就C而言,支持這一點不值得爲您提到的平臺提供不一致的支持,其中包括16位DOS和Windows(請記住近點和遠點指針? )畢竟,如果一個平臺,比如POSIX,想直接允許這個,他們可以把它作爲一個擴展。 – 2014-02-21 08:51:43

0

不像別人說什麼,是的,你可以有一個void*指針作爲函數指針,但語義是非常棘手與使用它。你可以看到,你不需要把它當作void*,只需要像正常一樣分配它即可。我像這樣運行你的代碼,然後我編輯它來工作。

file1.c中:

typedef unsigned long (*FN_GET_VAL)(void); 

extern FN_GET_VAL gfnPtr; 

void setCallback(const void *fnPointer) 
{ 
    gfnPtr = ((FN_GET_VAL) fnPointer); 
} 

file2.c中:

int main(void) 
{ 
    setCallback(myfunc); 
    ((unsigned long(*)(void))gfnPtr)(); 
}