有关进程空间,全局变量,静态变量

    【创新性声明】本文没有本质上的创新性内容。属于一些实验和总结,有少量主观推测成分(有待进一步证实)。写这一类文章是非常危险的,因为有很多东西可能是我们不了解和比较模糊的,这很可能会出现错误的主观臆测,不仅仅是令明真相者贻笑大方的问题,更可怕的在于传播“错误”,这是我最为诚惶诚恐的一点。比如,我之前见到我指点过他的sun先生对于Photoshop中置换滤镜中的他的那些主观错误结论已经传遍网络,尽管可能没有太多人能关注到这个层面,但是我还是为这些错误的观点在网络上比比皆是而深感遗憾。正因为此,这篇文章我受限于个人水平,也不能100%确保自己不犯下认知性错误,因此此本(1)需要更专业的人的监督和意见,(2)可能随着我自己认知的变化而进行修订和变更。

 

    最近在一些回复中提到了一些静态变量,线程安全性,递归函数调用的问题。尽管大概情况我已经清楚,但感觉自己在某些细节方面还稍显模糊,因此在这个问题应该从底层上做一个重新的总结,把一些比较容易产生模糊的问题总结下。

    (1)全局变量,和静态变量位于进程空间什么位置。

    为此,我用 VC6 新建了一个 Windows Console Application 。并输入代码如下:

test_code

#include <stdio.h>#include <stdlib.h>//#include <process.h>#include <tchar.h>#include <windows.h>

typedef HANDLE (WINAPI *FuncPtr)(    LPSECURITY_ATTRIBUTES lpThreadAttributes,    SIZE_T dwStackSize,    LPTHREAD_START_ROUTINE lpStartAddress,    LPVOID lpParameter,    DWORD dwCreationFlags,    LPDWORD lpThreadId    );

class TestClass{public:    TestClass()    {        printf("TestClass's constructor.\n");    }    ~TestClass()    {        printf("TestClass's destructor.\n");    }};

TestClass g_class;

int g_Temp1 = 0x11223344;int g_Temp2;int g_Array[4];int g_Temp3;

//The default size for the reserved and initially committed stack memory //is specified in the executable file header. DWORD WINAPI threadProc(LPVOID lpParameter){int a = 0;int threadIndex = (int)lpParameter;    printf("%p: auto variable on new thread's stack (thread: %d)\n", &a, threadIndex);return 0;}

int main(int argc, char* argv[]){static int nStatic1 = 0x12345678;static int nStatic2 = 0x00abcdef;static int nStatic3;

int a = g_Temp1 & 0xffff;char* pConstData = "Hello World"; 

//pConstData[1] = 0; //内存访问冲突(不可写)

char* pHeap1 = (char*)malloc(32);char* pHeap2 = (char*)malloc(32);

printf("---enter main----\n");    HMODULE hModule = GetModuleHandle(_T("Kernel32.dll"));    printf("%p: Module (Kernel32.dll)\n", hModule);

FuncPtr pFunc = CreateThread;    printf("%p: FuncPtr (CreateThread)\n", pFunc);

DWORD tid1, tid2;    HANDLE hThread1 = CreateThread(NULL, 0, threadProc, (LPVOID)1, CREATE_SUSPENDED, &tid1);    HANDLE hThread2 = CreateThread(NULL, 0, threadProc, (LPVOID)2, CREATE_SUSPENDED, &tid2);

ResumeThread(hThread1);    WaitForSingleObject(hThread1, 5000);    CloseHandle(hThread1);    ResumeThread(hThread2);    WaitForSingleObject(hThread2, 5000);    CloseHandle(hThread2);

printf("%p: auto variable on main thread's stack\n", &a);    printf("%p: memory alloc on heap: pHeap1 (32 bytes)\n", pHeap1);    printf("%p: memory alloc on heap: pHeap2 (32 bytes)\n", pHeap2);    printf("%p: address of const data(\"hello world\")\n", pConstData);    printf("%p: global variable g_Temp1 (initialized)\n", &g_Temp1);    printf("%p: global variable g_Temp2 (not initialized)\n", &g_Temp2);    printf("%p: global variable g_Array (not initialized)\n", g_Array);    printf("%p: global variable g_Temp3 (not initialized)\n", &g_Temp3);    printf("%p: static variable nStatic1 (initialized)\n", &nStatic1);    printf("%p: static variable nStatic2 (initialized)\n", &nStatic2);    printf("%p: static variable nStatic3 (not initialized)\n", &nStatic3);    free(pHeap1);    free(pHeap2);    printf("---leave main----\n");return 0;}

 

    在这个例子里我添加了全局变量,auto 类型的栈上临时变量,函数内的静态变量,以及新创建的线程的栈上的变量等。此程序产生的输入如下:

test_output

 1 TestClass's constructor. 2 ---enter main---- 3 7C800000: Module (Kernel32.dll) 4 7C810647: FuncPtr (CreateThread) 5 0052FFB0: auto variable on new thread's stack (thread: 1) 6 0062FFB0: auto variable on new thread's stack (thread: 2) 7 0012FF7C: auto variable on main thread's stack 8 00370FE0: memory alloc on heap: pHeap1 (32 bytes) 9 00371038: memory alloc on heap: pHeap2 (32 bytes)10 00423148: address of const data("hello world")11 00425B40: global variable g_Temp1 (initialized)12 00428D78: global variable g_Temp2 (not initialized)13 00428D68: global variable g_Array (not initialized)14 00428D64: global variable g_Temp3 (not initialized)15 00425B44: static variable nStatic1 (initialized)16 00425B48: static variable nStatic2 (initialized)17 00428D60: static variable nStatic3 (not initialized)18 ---leave main----19 TestClass's destructor.

 

    首先,我们可以看到一个类的实例作为全局变量,它的构造函数在进入入口点函数(main)之前被调用,在main函数之后被析构。这显示这个过程应该是包装在main函数外层的函数,也就是 CRT 做的。

    上面的输出给出了各个变量在进程空间中的地址(虚拟内存地址),因此我们再对照一下编译后的PE文件的 SectionHeader 的信息,使用我编写的文件格式查看器打开编译后的EXE文件,如下图所示:

    

    

 

    根据PE文件中的信息:

    ImageBase = 0x0040 0000;

    SectionAlignment = 0x1000;

    FileAlignment = 0x1000;

    可以根据 section header 的信息知道,各个段在进程空间中的地址是(对比程序输出,可知全局变量和常量字符串在那个段范围内):


section


From


To


Characteristics


备注


.text


00421790


00422790


可执行,可读


代码段


.rdata


00423000


00424777


可读


初始化数据段(常量)


.data


00425000


0042A730


可读,可写


初始化数据段(全局和静态变量)


.idata


0042B000


0042B7CC


可读,可写


 


.reloc


0042C000


0042CEC9


可读,可丢弃


 基址重定位

 

    上表是根据PE文件中的信息给出了进程空间中各个段的地址范围。因此我们根据运行时的输出情况,可以很容易的确定哪些变量具体位于进程空间的那个段中。例如针对这个具体的实例来说,全局变量和函数内静态变量位于 .data 段,常量字符串位于 .rdata 。 

    注意由于 SectionAlignment 和 FileAlignment 是相同的,因此文件中的内容和映射后,节之间的相对位置是没有变化的。节的特性被设置给内存页,因此如果代码中试图对内存执行没有权限的操作,例如对常量字符串的地址进行写入,系统就会告知错误。可以看到 section 的起始地址使用 sectionAlignment (默认 0x1000)对齐。数据段默认不具有共享属性,被映射以后数据是该进程空间所私有的。但共享的段则在所有进程实例中共享。可以设定某个段为共享,当使用一个DLL时,则数据通过DLL来给所有进程共享,共享意味着在所有进程空间中,该段被映射到相同的物理内存页。例如《windows 核心编程》中有一个例子,当创建多个进程实例,对话框上能实时更新的显示出当前实例的个数。(同时这也是作为用于控制只能运行一个进程实例的方法之一):

 

// Tell the compiler to put this initialized variable in its own Shared // section so it is shared by all instances of this application.//#pragma data_seg("Shared")volatile LONG g_lApplicationInstances = 0;#pragma data_seg()

// Tell the linker to make the Shared section readable, writable, and shared.#pragma comment(linker, "/Section:Shared,RWS")

 

    可以看出常量字符串代码中的"Hello World”位于.rdata节,该节是不可写的。全局变量和静态变量都位于.data节,可读可写。代码中已初始化的数据在节内位于靠近节前部地址较低的地方,未初始化的数据靠地址较高的地方。对于程序中的基本类型变量例如整形等,实际上它们从文件中被读入内存后就已具有了初值,相当于完成了初始化。但对于一个类的实例来说,对其初始化需要调用其构造函数。这些都需要在进入入口点函数之前完成。

    因此现在我们可以明确:

 

    char str[] = "Hello World";

    含义是 str 数组是位于栈上的数组,然后从初始化段(只读)拷贝字符串内容到栈上的数组。(编译器可能对其用 DWORD 进行拷贝)

 

    char *str = "Hello World";

    str 是位于栈上的一个指针,指向初始化段的只读数据。

 

    初学者一般不太容易立即区分出上面的代码在底层上的区别:前者是栈上的数组,数组空间在栈上分配。编译器会插入代码,在运行时把数据从只读数据段拷贝到栈上空间。所以前者的 str 是可写的。后者是栈上的一个指针变量,指向只读数据段上的某个位置(在C++里面这样的赋值合法,是为了照顾成千上万的已有 C 代码,此处参考了参考资料(5)《The C++ Programming Language》中的叙述)。后者的 str 是不可写的。如果试图修改它,在编译上是通过的,但运行时属于不确定行为。

 

    我先后创建两个新的线程,是为了观察新线程的栈的位置,可以看到在这个例子中,在创建时我没有指定栈的大小,默认是 1MB。从低地址往高地址数,主线程的栈在进程空间中相当靠前,(中间的空隙属于堆),然后是那些 sections,然后此处剩下很大的一段空隙又基本上全部是属于堆。然后是新创建的线程的栈(默认 1MB),它们的地址看起来已经相当高。然后是那些被影射进来的DLL,系统 DLL 都经过了 Rebase,一般位于比较高地址的地方(靠近 2GB 边界),最后是系统控制的空间边界(0x80000000)。当然,默认情况下,是程序员和系统各占 4GB 空间的一半,但可以通过启动的配置参数让程序员负责的空间达到 3GB。因此上面的范例的进程空间大体如下图所示:

 

    

 

    当然这个图只是针对这个控制台程序例子的特例而绘制,只是给出该例子中进程空间的示意图。图中箭头表示的是栈的增长方向。当一个进程中运行多个线程时,它们各自拥有独立的栈。当然这些栈之间由于位于相同的地址空间,所以它们彼此是可见的。因此一个线程可以把自己的栈上地址传递给其他线程进行处理,但这种地址随着线程退出就会失效,因此必须做线程同步,即阻塞式等待。例如不能使用 PostMessage 传递栈上的地址给其他窗口的窗口过程,而使用 SendMessage 是可行的。

    当我们提到并发性和多线程安全,通常在针对一个函数讲时,是进程空间内的多个线程同时调用该函数是否会引发问题。并发还指多个线程对同一资源的访问和使用,例如数据库,文件等。如果一个函数是可重入的,则它不能直接或简介的使用,引用静态,全局性变量(尤其是对这些共享性变量的读写不是原子性的,且这些变量的值的准确性如果对于使用者来说是关键的状态性数据,则会引入诸如“脏读”等问题),也不能直接或简介调用不可重入的函数。栈上的一个 auto 类型变量是多线程安全的,是因为栈是属于线程的,彼此独立,因此 auto 类型变量对于线程来说相当于“私有数据”。对于递归函数,每一层递推都会在栈上增加一层 stackframe,因此函数内的临时变量位于其各自调用所属的 stackframe 中,也相当于彼此独立。

 

    (2)系统调度,消息队列的基本单位。

    这里我们提及的是进程和线程,不再考虑线程以下的概念。考虑下面的问题:

    2.1 拥有消息队列的基本单位是什么?A。窗口;B。线程。

    答案是线程。但通常我们更容易把消息队列和窗口关联在一起,这仅仅是因为事件驱动给我们的一种主观表象。由于调度的基本单位是线程,因此处理消息和拥有消息队列的基本单位也是线程。系统派送消息的目标,是把消息送到创建窗口的线程所拥有的消息队列。然后该线程被调度得到时间片,由消息循环(DispatchMessage)间接调用该窗口的窗口过程函数。但一个线程不一定拥有消息队列,例如只专注于运算不负责接受消息控制的后台线程,只有线程调用 PeekMessage,GetMessage 一类的函数时,系统才会为这个线程创建消息队列。拥有消息队列的线程也不一定会创建窗口,但是创建窗口的线程就会接收到和窗口有关的各种消息。

    总结一下,线程中根据其功能又可以大致的分为主线程(或者在GUI程序中通俗的理解为 UI 线程)和后台线程。消息中的一部分属于窗口消息,这些消息是和一个具体的窗口关联并以创建该窗口的线程的消息队列作为目标的(最终线程的消息循环负责引发对窗口过程的调用,即 DispatchMessage),但并不是所有的消息都会和具体窗口关联。例如普通的线程消息(而一个线程也可能不会创建任何窗口)。窗口消息中的一部分又可以属于对话框消息。

 

     2.2 比较特殊的几个消息:WM_MOUSEMOVE, WM_TIMER, WM_PAINT, WM_QUIT; (待完成)

    线程消息分为三种类型:incoming sent message; posted message; input message; 在标准术语里面,第一种被称为非队列消息,后两种称为队列消息,也就是被理解为“位于消息队列中的消息”。

    参考资料:

    (1)《程序员的自我修养》,俞甲子 等。

    (2)“Billy Belceb 病毒教程Win32篇”(选自《看雪学院五周年纪念收藏版》), Billy Belceb。

    (3)《windows核心编程》, Jeffery Richer.

    (4)《Tho Old New Things》, Raymond Chen.

    (5)《The C++ Programming Language》,BS。

    



    【补充说明】

    本文已有的评论,因为涉及到有具体所指的个人主观性意见,于技术探讨交流上没有任何积极意义,因此不在技术博客保留,已转移到163博客的以下地址进行备份性保留(可以点击以下地址观看原有评论):

 

    http://blog.163.com/jinfd@126/blog/static/62332277201211104431759/

 

    本文评论在此处被删除的原因是,其内容和技术或讨论主题无关(例如无实质内容,明显针对人的主观评价等)。

    --hoodlum1980,2012年2月1日。

 

时间: 2024-08-02 11:37:42

有关进程空间,全局变量,静态变量的相关文章

php 定义全局变量,静态变量,局部变量定义实例

 //$globals 数组    function testfunction() {   echo $globals["php_self"];  }  testfunction();     // 本程序使用全局定义  function testfunction() {   global $s;   echo $s;  }  $s='this is www.111cn.net';  testfunction();    // 静态变量的例子  function testfunctio

Java静态变量 实例变量 静态方法详解

首先语法定义上的区别:静态变量前要加static关键字,而实例变量前则不加. 在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量.静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了.总之,实例变量必须创建对象后才可以通过这个对象来使用  代码如下 复制代码 package staticVar;    public class s

Android静态变量的生命周期 简单介绍_Android

Android是用Java开发,其静态变量的生命周期遵守Java的设计.我们知道静态变量是在类被load的时候分配内存的,并且存在于方法区.当类被卸载的时候,静态变量被销毁.在PC机的客户端程序中,一个类被加载和卸载,可简单的等同于jvm进程的启动和结束.那么在Android中呢?用的Dalvik vm也是一样的.不过Android不太突出的进程概念,所以对静态变量的生命周期就会感觉模糊,这种模糊对于值类型是无所谓的,如果是静态的对象引用,则与内存回收.内存泄漏这些问题有关,有必要加深研究和理解

java中静态变量和实例变量的区别详细介绍_java

运行效果: 控制台效果: ================================================== 代码部分 ================================================== /hello_test/src/com/b510/test/StaticTest.java 复制代码 代码如下: /**  *   */ package com.b510.test;  /**  * 在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实

Android静态变量的生命周期 简单介绍

Android是用Java开发,其静态变量的生命周期遵守Java的设计.我们知道静态变量是在类被load的时候分配内存的,并且存在于方法区.当类被卸载的时候,静态变量被销毁.在PC机的客户端程序中,一个类被加载和卸载,可简单的等同于jvm进程的启动和结束.那么在Android中呢?用的Dalvik vm也是一样的.不过Android不太突出的进程概念,所以对静态变量的生命周期就会感觉模糊,这种模糊对于值类型是无所谓的,如果是静态的对象引用,则与内存回收.内存泄漏这些问题有关,有必要加深研究和理解

浅谈Android中关于静态变量(static)的使用问题

项目中,在不停地接收串口数据很长一段时间(几小时)后,会偶然性的报错.初步排除了oom和cpu紧张问题,因为是工业平板不方便调试,用了些比较笨的方法最后锁定在几个用了static的地方.在这里记录下Android中使用static的一些问题. 静态变量的生命周期遵守Java的设计.静态变量在类被load的时候分配内存,并存在于方法区.当类被卸载时,静态变量被销毁.在PC机的客户端程序中,一个类被加载和卸载,可简单的等同于jvm进程的启动和结束.在Android中,用的DVM也是一样的,不过And

linux进程的堆栈空间_代码段(指令,只读)、数据段(静态变量,全局变量)、堆栈段(局部变量)、栈【转】

转自:http://blog.csdn.net/gongweijiao/article/details/8207333 原文参见:http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201215115845553/    一)概述 .堆栈是一个用户空间的内存区域,进程使用堆栈作为临时存储.   .堆栈中存放的是函数中的局部变量,在函数的生命周期中可以将变量压入堆栈,编译器需要确保堆栈指针在函数退出前恢复到初始位置,也就是说,内存是自

C/C++中全局变量和静态变量的区别

1. 全局变量的作用域是整个项目,它只需要在一个源文件中定义就可以作用于所有的源文件,其它不包括全局变量定义的文件需要用extern关键字再次声明这个全局变量. 2. 全局变量.静态全局变量.静态局部变量都是在静态存储区(全局数据区)中分配空间的,而局部变量是在栈上分配空间的. 3. 全局变量.静态变量的生命期和程序生命期是一样的,在程序结束之后操作系统会回收空间. 4. 全局变量和静态变量都是保存在静态存储区中,生命期和程序一样,但是不同的是全局变量的作用域是整个项目,而静态全局变量是当前程序

全局变量与全局静态变量的区别

全局变量与全局静态变量的区别: (a)若程序由一个源文件构成时,全局变量与全局静态变量没有区别. (b)若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享,即:全局静态变量对组成该程序的其它源文件是无效的. 静态全局变量的作用: (a)不必担心其它源文件使用相同变量名,彼此相互独立. (b)在某源文件中定义的静态全局变量不能被其他源文件使用或修改. 例如:一个程序由两个源文件组成,其中在一个源文件中定义了"int n;",在另一个源