深入理解靜態變量

本文爲看雪論壇優秀文章

看雪論壇作者ID:flag0

全局靜態變量

  • 數據存儲:

    • 已初始化的存儲在數據區中的已初始化變量區。

    • 未初始化的存儲在數據區中的未初始化變量區。

  • 作用域:文件作用域。

  • 本質:是受編譯器按語法約束的全局變量。

  • 作用:私有化某些變量和方法,以文件爲單位對源碼進行控制和管理。

  • 生命週期:從所處模塊裝載到所處模塊卸載。

探測全局靜態變量生命週期

首先打印出全局變量的地址。

#include <stdio.h>
#include <stdlib.h>
 
static int g_nTest = 0x996;
 
int main()
{
    printf("%p\r\n",&g_nTest);
    system("pause");
    return 0;
}

 

在mainCRTStartup()函數起始位置下斷點,然後在內存窗口監測靜態全局變量地址。

 

 

單步步過,尋找影響全局靜態變量內存地址的語句。

 

 

可以看到其在斷下時,全局靜態變量地址的值就已經有了,因爲已初始化的全局變量的值會被寫入到exe文件中,所以其在模塊加載時,就已經有了值,是在mainCRTStartup()函數之前的。

 

我們繼續測試,在C++編譯器環境下,將函數的返回值賦值給全局靜態變量的情況。

#include <stdio.h>
#include <stdlib.h>
int GetInt()
{
    printf("Hello world!");
    return 0x996;
}
 
int nTest1 = GetInt();
 
int main()
{
    system("pause");
    return 0;
}

 

該函數在_cinit()中的第二個_initterm調用裏被執行,_cinit()的作用爲初始化浮點協處理器和初始化全局變量。

 

 

F11跟進_cinit:

 

 

此時到了第二個_intitterm按F10(不要按F11跟進去)自動跳轉到在GetInt函數頭部下的斷點的位置。

 

 

第一個爲_initterm官方的全局變量初始化,第二個_initterm才爲用戶的全局變量初始化。

 

全局變量結束

我們繼續探測全局變量的值被釋放的結束的地方。

 

在main函數return處下斷點,單步步過到進程結束的位置,查看全局靜態變量值的變化。

 

一路F10跟到MainCRTStartup中的exit(mainret);處,全局靜態變量內存的值仍未發生變動,此時單步執行exit時,程序結束。

 

 

所以,我們可以判定,全局變量的生命週期是從所處模塊裝載到所處模塊卸載

編譯器控制跨文件訪問:限制導出

全局靜態變量主要用途就是限制導出,實現其函數和變量的私有化,編譯器通過限制導出機制來控制其跨文件訪問的。

 

導入:使用其他模塊中的符號。

 

導出:提供某個符號給其他的模塊用。

 

例如:靜態函數

 

static void foo(),只能在本文件中使用,不可以跨文件調用,這樣則有利於開發過程中的私有化,從而摘輕各自開發者的責任。

 

早期編譯器的私有概念是通過static來實現的,後來才完善這個概念,並逐步發展爲其他的面嚮對象語言,比如C++。

 

在沒有面向對象概念的時候,使用static來實現私有化。

 

 

使用限制導出思想的demo

main.c:

static char* msg = "Hello";
char* GetMsg()
{
    return msg;
}

Test.c:

printf("%s\r\n",GetMsg());

 

控制跨文件訪問

編譯器編譯階段將全局靜態變量進行處理,在鏈接階段時候,其他文件便不能夠訪問本文件中的全局靜態變量了,會產生報錯。

 

 

但是僅僅是編譯器層面做的處理,全局靜態變量的值依舊存在內存中,可以用如下的方法進行訪問。

 

main.cpp:

#include <stdio.h>
#include <stdlib.h>
 
static int g_nTest = 0x996;
int g_nTest2 = 0x123;
void printFun();
 
int main()
{
    printFun();
    //printf("%p\r\n",&g_nTest2);
    system("pause");
    return 0;
}

Test.cpp:

#include <stdio.h>
extern int g_nTest2;
 
void printFun()
{
    printf("%x\r\n",(&g_nTest2)[-1]);
}

局部靜態變量

  • 數據存儲:

    • 已初始化的存儲在數據區中的已初始化變量區。

    • 未初始化的存儲在數據區中的未初始化變量區。

  • 作用域:與所在函數作用域相同。

  • 生命週期:與全局靜態變量相同。

  • 作用:局部靜態變量可以在過程或函數重複運行的時候保留上次運行的值。

名稱粉碎

名稱粉碎(Name-mangling)又名命名粉碎或命名重組,是指在目標文件符號表和連接過程中使用的名字通常與編譯目標文件的源程序中的名字不一樣,編譯器將目標源文件中的名字進行了調整。

 

編譯器對局部靜態變量使用了名稱粉碎機制。

 

首先將其聲明成全局變量,然後將其作用域插入到全局變量名稱中去,類似於snTest_fooD通過這種方式將全局變量限制爲在某函數裏面纔可以訪問。

 

不同編譯器廠商對局部靜態變量的名稱粉碎機制存在差異,有些會將參數和返回值也加入到重組後的名稱中,名稱粉碎和編譯器廠商的習慣相關,不屬於標準,所以,不同的廠商不同的版本,甚至不同的版本規則都不一樣。

 

 

編譯器的名稱粉碎機制測試方法

修改各項函數屬性,編譯後,打開對應的obj文件,搜索局部靜態變量名,查看不同屬性參數的修改對於名稱粉碎後的局部靜態變量名的影響。

#include <stdio.h>
#include <stdlib.h>
 
void TestLocal()
{
    static int nTest1 = 0x996;
    printf("%d\r\n",nTest1);
}
 
 
int main()
{
    TestLocal();
    system("pause");
    return 0;
}

將以上代碼編譯稱爲obj文件。

 

打開obj文件,搜索局部靜態變量名nTest1:

 

 

其在vc6.0的c編譯器下的名稱粉碎爲:

 

_?nTest1@?1??TestLocal@@9@9

 

將其局部靜態變量放入函數內的代碼塊中,編譯後觀察名稱粉碎的變化:

void TestLocal()
{
    {
        static int nTest1 = 0x996;
        printf("%d\r\n",nTest1);
    }
}

其名稱粉碎後的結果爲

 

_?nTest1@?2??TestLocal@@9@9

 

可以看到由?1變成了?2這裏大致可以推測,?x表示層級。

 

名稱粉碎識別關鍵參數

  • 變量名

  • 作用域名

  • 作用域的層級編號

全局靜態變量不進行名稱粉碎不影響從標識符到內存地址的識別,局部靜態變量不名稱粉碎會影響。

 

編譯器通過名稱粉碎的方式做語法檢查,關鍵是集成了變量名、作用域名、作用域的層級編號。

局部靜態變量只能被賦一次初值的原因

static int snTest = 999;

上述代碼是給編譯器看的,告訴編譯器全局變量的snTest的初值爲999。

 

靜態局部變量定義處沒有產生賦值的彙編代碼,所以在函數執行時不會被賦值。

 

局部靜態變量初始化爲常量的值

靜態局部變量如果賦初值,則會和已初始化的全局變量一樣被寫入到文件中,存儲在數據區中的已初始化的全局變量區。

 

 

查看exe文件26a30處:

 

 

如果未賦初值,則會存儲在未初始化的全局變量區,都不會產生賦值的彙編指令。

 

局部靜態變量初始化爲變量的值

void fooD(int n)
{
    static int nTest = n;
}

在C編譯器下報錯error C2099: initializer is not a constant

 

在C++編譯器環境下

 

c++的語法允許局部靜態變量初始化爲變量的值,c語言不允許。

 

當採用C++編譯器時,名稱粉碎規則會發生改變。

 

調用方式、返回值、函數參數、及函數參數的數量均會影響到其名稱粉碎規則的改變。

 

 

_?nTest1@?1??TestLocal@@YAXH@Z@4HA

 

VC++6.0 Debug中watch窗口解析名稱粉碎bug

 

watch窗口用的C編譯器的名稱粉碎規則,所以其無法正常顯示cpp文件中的局部靜態變量信息。

 

 

當靜態局部變量賦初值爲變量時,儲存在未初始化區,會產生代碼。

 

會產生彙編代碼:

 

 

存儲在未初始化全局變量區:

 

如何判斷靜態局部變量是否被賦初值

當靜態全局變量賦值爲變量之後,VC++6.0編譯器會在其存儲位置附近增加一個字節來存儲是否賦初值的狀態。

 

VC++6.0中,一個位存儲一個靜態全局變量是否被賦初值的狀態。

 

其他編譯器存儲狀態的位置和大小可能不一樣,但是思路一樣。

#include <stdio.h>
#include <stdlib.h>
 
void TestLocal(int n)
{
    static int nTest2 = n;
    printf("%p:",&nTest2);
    printf("%d\r\n",nTest2);
    (&nTest2)[1] = 0;
    nTest2++;
}
 
int main()
{
    TestLocal(10);
    TestLocal(20);
    TestLocal(30);
    system("pause");
    return 0;
}

(&nTest2)[1] = 0;將這個標誌位的值給修改掉了,所以導致了靜態變量重複賦初值。


在VC++6.0編譯器中,當賦初值爲函數參數的局部靜態變量超過8個時,會新增加一個字節來記錄狀態:

致謝

科銳逆向 錢林松老師

 by科銳37期學員


- End -


看雪ID:flag0

https://bbs.pediy.com/user-873556.htm 

*這裏由看雪論壇 flag0 原創,轉載請註明來自看雪社區。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章