C#如何获取引用本身存于线程堆栈上的地址!大神来!

右右yy 2014-01-08 10:23:31
C#引用类型本身是存在于线程的堆栈上的,而指向的对象是存在于托管堆上的。由于GC压缩机制,在此对象不被销毁的情况下,引用的地址也是有可能变化的,所以获取引用地址没有什么意义。
但我想获取的不是引用变量的值,而是引用变量本身在内存中的地址!由于存于栈上,所以在对象有效作用域内,这个地址是不会变的。有大神知道如何获取么?用什么函数?我在我的C#工程中把某引用本身的地址获取到,然后传给C++DLL函数。
...全文
663 点赞 收藏 64
写回复
64 条回复
右右yy 2014年05月06日
一直未来结贴。 本想近日研究C#一段时日,不甚清楚的再来和Susiria探讨。无奈最近工作局限于C++和QT,无瑕继续C#方面工作了。 感谢Susiria同志,你的很多回帖使我收获很大,感谢你的各种耐心回帖,谢谢!结贴了。
回复 点赞
右右yy 2014年01月15日
引用 62 楼 Susiria 的回复:
。。。。。 综上,.Net采用的固定对象的机制是正确的。
受教了,GC移动托管堆对象时,会挂起所有托管代码中的线程,但阻不了C++,所以这个逻辑自然也不成立了。 关于P/Invoke我这几天再看看,得看看何时才需用GCHandle....与C++交互的某些情况下是需要GCHandle等来进行固定操作的,得搞搞明白是在哪些情况下.
回复 点赞
Susiria 2014年01月15日
1 .Net的确有这种机制,在与非托管代码交互时自动定址: “因为当回收发生时,.NET Compact Framework 和完整的 .NET Framework 中的垃圾回收器都可能在托管堆上重新安排对象,所以如果非托管函数试图使用传递给它的指针操作内存,就可能出现问题。幸运的是,.NET Compact Framework 的封送拆收器在调用期间将自动锁定(将对象锁定在其当前内存位置)任何传递给非托管函数的引用类型。”(http://msdn.microsoft.com/zh-cn/library/aa446536.aspx) “The managed function suppresses garbage collection for the managed delegate to prevent .NET Framework garbage collection from relocating the delegate while the native function executes.”(http://msdn.microsoft.com/en-us/library/ektebyzx.aspx) 而且正如49楼第一点意见所言,如果原本没有这种机制,无数的.Net程序早就出问题了。 2 无论传递的是指针,还是指针的指针,假如不固定对象的话还是可能出问题。GC线程是一个单独的线程,要移动一个对象然后更新栈上的引用,必须有线程同步机制,以保证在此过程中另一个线程不会同时操作。我猜测可能是采取lock机制,即在移动之前,锁定引用,然后移动对象,然后解除锁定。在解锁之前,托管代码对该引用的操作都暂时挂起,这些都是对托管代码采取的机制。 但指针不是托管代码,并不能享受这个好处,在非托管代码的执行过程中,GC仍有可能移动对象。GC显然没有办法对非托管指针采取锁定并进行同步,非托管代码也无法接受GC移动对象的通知,所以非托管代码访问的将是被释放的内存。 综上,.Net采用的固定对象的机制是正确的。
回复 点赞
右右yy 2014年01月14日
引用 60 楼 Susiria 的回复:
根据微软的博客,驻留的字符串不会被GC移动,因为GC无法对此区域进行垃圾回收: "After a string is interned, it is being referenced by a special array, to prevent it from being garbage collected." (http://blogs.microsoft.co.il/levi_moshe/2012/10/08/intern-pool-improvements-between-various-net-framework-versions/) 但我觉得你的问题可能与字符串驻留无关,毕竟只有在编译期显式声明的字符串(literal),和已编程的方式调用string.Intern时才会驻留,动态的字符串肯定不会默认被驻留的。真正的原因可能是在交互之前,CLR内部已经做了工作,以防止对象被GC移动: “如果准备在非托管代码中使用指针传递一个引用对象的地址,将会面临垃圾收集器背着你移动该引用对象的风险。还好P/Invoke机制能检测到这种情况。在这种情况下,它会将对象在内存中定址,然后提供一个指向本级代码的指针。当我们从方法中返回后,该对象会自动解除固定。” ( C#和.NET2.0实战——平台、语言与框架,P193) 那么,你之前所提出的问题,究竟是对可能出现的现象的怀疑,还是亲自测试并发现到这个现象呢
你说的没错..我想起来只有静态字符串会驻留..动态没戏. 关于P/Invoke我还在看,学习这机制究竟是什么。你说这机制的意思是:C#调用非托管方法时,自己做固定地址的操作?那我们调用C++前岂不是不用GCHandle、fixed和栈上分配,这一切的担心都是不必的了?这个概念是颠覆性的。 我是由于GC机制才有此怀疑,我的程序跑几年并未出异常,所以我想搞明白传递本质。 另外有段逻辑你看看哪里有问题: 如果C#调用C++时传递的是ref引用,传到C++后即是地址的地址,所以就算GC移动了对象,C++也完全可知这对象的新位置,就是因为传递的是地址的地址。所以,如果是以ref传递,根本都可以不去考虑GC是否会移动,移不移动C++都可获取正确的对象数据。 感谢你的耐心回答,就要快结贴了..感谢你提供的相当准确的参考资料
回复 点赞
Susiria 2014年01月14日
根据微软的博客,驻留的字符串不会被GC移动,因为GC无法对此区域进行垃圾回收: "After a string is interned, it is being referenced by a special array, to prevent it from being garbage collected." (http://blogs.microsoft.co.il/levi_moshe/2012/10/08/intern-pool-improvements-between-various-net-framework-versions/) 但我觉得你的问题可能与字符串驻留无关,毕竟只有在编译期显式声明的字符串(literal),和已编程的方式调用string.Intern时才会驻留,动态的字符串肯定不会默认被驻留的。真正的原因可能是在交互之前,CLR内部已经做了工作,以防止对象被GC移动: “如果准备在非托管代码中使用指针传递一个引用对象的地址,将会面临垃圾收集器背着你移动该引用对象的风险。还好P/Invoke机制能检测到这种情况。在这种情况下,它会将对象在内存中定址,然后提供一个指向本级代码的指针。当我们从方法中返回后,该对象会自动解除固定。” ( C#和.NET2.0实战——平台、语言与框架,P193) 那么,你之前所提出的问题,究竟是对可能出现的现象的怀疑,还是亲自测试并发现到这个现象呢
回复 点赞
showjim 2014年01月14日
引用 58 楼 u012211533 的回复:
“虽然单纯的通过纯值类型引用是不能获取到引用类型的地址” 这句的意思是..? 我想获得栈上变量的地址信息,比如一个引用类型自身只占用4字节(32位系统),是在堆栈上开辟的,我想得到这个4字节内存的首地址。
纯值类型 比如 int。 非纯值类型 比如 object、包含object的结构体、泛型、其它不确定类型。 这两种局部变量是保存与两个不相关的堆栈上的。如果不分开,GC无法识别某个数据到底是数据还是引用指针。GC只处理后面这个堆栈,它们都被定义为指针,同时目标绑定类型。有的数据比较特殊,比如包含object或者非确定类型的结构体,指针保存在后面的堆栈上(仅用于GC扫描),数据+类型则可能保存在前面那个堆栈上。
回复 点赞
Susiria 2014年01月13日
就我来说,没有见过不用unsafe就能取得内存地址的方法,也没有framework内置方法(GCHandle的AddrOfPinnedObject只能取得固定对象的地址) string是很特殊的引用类型,字符串是任何程序都经常用到的类型,所以CLR对它有特殊的优化: “当CLR初始化时,它会创建一个内部的散列表,其中的键为字符串,值为指向托管堆中字符串的对象的引用...垃圾收集器不会释放CLR内部的散列表中引用的字符串对象,这些String对象的引用一直被散列表保存着。只有当进程中所有的应用程序域都不再引用这些字符串对象时,它们才会被释放。。。”(CLR via C#) 所以我认为,你说的不固定字符串也没有出错的情况,是因为这个字符串原本是在散列表之中的。
回复 点赞
largeskymengsk 2014年01月13日
第一个问题:你考虑到了.net的对象,随时会被移动。难道微软就考虑不到?而且无数的.net程序使用了很久出问题了吗?没有吧。要搞清楚这个问题,你需要知道.net在调用C++时候,如何处理托管内存与非托管内存的交互,两种方式:固定与复制。 第二个问题:使用ref表示表示传递指针。ref int表示:int *p,ref string 表示char **。当然使用ref最多只能传递我们在C++中所用的二级指针。有了一,二级指针,你的问题还解决不了吗? 那么,你需要更多级指针,更复杂的操作吗?使用marshal类手动处理内存的交互也是可以实现的。 建议,永远不要去用unsafe代码,或者 fixed功能。
回复 点赞
右右yy 2014年01月13日
引用 46 楼 gomoku 的回复:
[quote=引用 5 楼 u012211533 的回复:] 大侠,那我如何才能实现C#传递int数组内容到C++中呢? ... /quote] 可以直接传int[],CLR保证在单个PInvoke期间,那个数组是不会被GC移动。比如:

// C++
int Sum(int* numbers, int count)
{
    int sum = 0;
    for(int i=0; i<count; i++) sum += numbers[i];
    return sum;
}
// C#
int Sum(int[] numbers, int count);
而ref int[]是基本上不应该出现的。因为ref in[]对应C++中的int**,而int**一般用来返回一个数组的:

int SetData(int** ppData)
{
   *ppData = (int*)malloc(1000 * sizeof(int));
   //...
   return 1000;
}
由于C++的指针不包含长度信息,它是不能直接转换为C#的数组的(C#的数组有长度信息)。因此不能用ref int[]来调用SetData。你可能试验过ref int[]可以用来传入数据。但关键一点是,如果只是用来传入数据,为何不直接用int[]来对应int*呢?
你好,之所以之前用ref来传递引用类型,是顾忌到引用类型很有可能要被GC移动,这int[]值自然也会变,C++拿到的是个无效的地址。而ref传过去后,对象再怎么在托管堆中移动,C++也可清楚知道,因为C++得到的是指针的指针。 你的意思是C++函数调用这段期间GC不会移动对象?那么不就是等于可以“直接传递引用类型”了么!要知道那个caozhy版主一直在这个贴里执着的说“不能传引用类型”,虽然他没提什么有价值的建议,但这点我也是认同的,我下午可以用int[]直接传给C++int*试一试,我印象中我试过,这是不行的,我再去拿代码验证下,谢谢你。
回复 点赞
右右yy 2014年01月13日
引用 44 楼 Susiria 的回复:
另一种方法是直接获取指针的地址,这也是你一直要求的,我谈谈我的思路,不知是否可行: //这是你要用的托管对象 string str = "abc"; //额外分配一个值,此时栈顶是strAddress int strAddress = 0; //进入unsafe作用域 unsafe { int* p = &strAddress;//取地址 IntPtr intP = new IntPtr(p); strAddress = intP.ToInt32(); //strAddress保存的是他自己的地址 } //退出unsafe作用域,栈顶回到strAddress //获取后面的str指针的地址 strAddress -= sizeof(int);
你的回复一直是让我觉得这贴中收获最大的..谢谢。 在不用unsafe的情况下,可否有方法可获知值类型和引用类型在堆栈上的地址呢?也就是用系统函数能求得一个变量在栈上的地址值。(引用类型本身也是存在于堆栈上) 另外,对于我一直想执着查明ref引用是否可用的原因:咱们说的GCHandle、fixed和栈上分配,其实都是为了做到:"固定指针"。但我现在接触的项目中,发现前人竟在没有进行固定引用的情况下,直接将数据以ref string形式传递给了C++,这程序已跑了好几年,没有出现这方面错误。但我知道固定指针的概念,所以觉得直接ref引用,貌似不妥,但是我经代码测试,发现确实是正确传递,没有错误。 所以现在这程序的情况是:没有固定引用的情况下,就进行ref string传值给C++,而且现象正确,数据正常。我真的想知道这到底是不是隐患...
回复 点赞
gomoku 2014年01月13日
[quote=引用 5 楼 u012211533 的回复:] 大侠,那我如何才能实现C#传递int数组内容到C++中呢? ... /quote] 可以直接传int[],CLR保证在单个PInvoke期间,那个数组是不会被GC移动。比如:

// C++
int Sum(int* numbers, int count)
{
    int sum = 0;
    for(int i=0; i<count; i++) sum += numbers[i];
    return sum;
}
// C#
int Sum(int[] numbers, int count);
而ref int[]是基本上不应该出现的。因为ref in[]对应C++中的int**,而int**一般用来返回一个数组的:

int SetData(int** ppData)
{
   *ppData = (int*)malloc(1000 * sizeof(int));
   //...
   return 1000;
}
由于C++的指针不包含长度信息,它是不能直接转换为C#的数组的(C#的数组有长度信息)。因此不能用ref int[]来调用SetData。你可能试验过ref int[]可以用来传入数据。但关键一点是,如果只是用来传入数据,为何不直接用int[]来对应int*呢?
回复 点赞
右右yy 2014年01月13日
引用 42 楼 hdt 的回复:
托管与非托管的交互,注意两点, 1、二者运行在两个不同的地址空间。 2、托管有gc,也就是说.net框架要完全控制托管堆。 所以,托管传递给非托管参数,如果是这个参数托管的只能是复制,而不能是引用或地址,因为如果是地址的话,非托管就可以改变这个地址的内容,达到破坏gc的效果, 托管方只能传一个非托管的内存的地址或引用到非托管方 你可能会有疑问例如调用 时会有 ref int[] 之类的参数,其实这样写.net框架会自动把这个地址转换。
你好,你的回复让我对于ref引用传递给C++的概念有了颠覆性改变。 我的概念是:ref引用传递的是引用本身的地址,所以传递到C++后,就算GC移动了C#托管堆中这个引用对象的位置,由于传递的是ref引用,所以C++亦可知道这对象的新地址,因为C++知道这个引用变量自身的地址,自然引用的内容(指向托管堆对象的位置)再怎么变,C++也完全可以获知了。所以C++可以准确获取托管堆上的信息。 而你说ref引用传递时,已经把托管堆上对象复制到了栈上?!再传递的是一个指向栈上对象的指针的指针?即如果C#中有一个int[]对象,如果要ref形式传给C++,则其会自动在栈上开辟空间来复制这个Int[]对象,然后再传过去....?
回复 点赞
Susiria 2014年01月13日
另一种方法是直接获取指针的地址,这也是你一直要求的,我谈谈我的思路,不知是否可行: //这是你要用的托管对象 string str = "abc"; //额外分配一个值,此时栈顶是strAddress int strAddress = 0; //进入unsafe作用域 unsafe { int* p = &strAddress;//取地址 IntPtr intP = new IntPtr(p); strAddress = intP.ToInt32(); //strAddress保存的是他自己的地址 } //退出unsafe作用域,栈顶回到strAddress //获取后面的str指针的地址 strAddress -= sizeof(int);
回复 点赞
Susiria 2014年01月13日
引用 38 楼 u012211533 的回复:
[quote=引用 37 楼 Susiria 的回复:] 楼主看下System.Runtime.InteropServices.GCHandle: 用 GCHandle 创建一个固定对象,该对象返回一个内存地址,并防止垃圾回收器在内存中移动该对象。 object varb = new int[] { 2, 3, 4 }; //GCHandleType.Pinned允许使用固定对象的地址。这将防止垃圾回收器移动对象 GCHandle handle = GCHandle.Alloc(varb, GCHandleType.Pinned); IntPtr address = handle.AddrOfPinnedObject(); //传递给c++ handle.Free();
天!感谢!高人指出了除stackalloc和fixed的第三种方案!终于在这个贴中有收获了。 高人啊,那“用GCHandle固定,然后最后手动free”这种方案好,还是"unsafe中fixed来固定"这种方案好呢?另外是不是stackalloc这种在栈上开辟内存的方案最不可取呢(如果传递的数据多)?因为栈的空间较少较宝贵。 真的谢谢。[/quote] --------------------------- GCHandle固定和fixed固定孰优孰劣我不太清楚,但fixed只能出现在unsafe代码中,GCHandle则不受限制; 你也说了,因为托管堆里已经分配了内存,你不想再在stack上重新分配一次。所以我支持不在stack上重新开辟内存,而是传递一个指针给c++; 当然,如果数据量不大,且不会频繁地被复制(作为函数参数或返回值)的话在stack上分配也没有关系,因为在堆里分配内存的代价要大于stack
回复 点赞
右右yy 2014年01月13日
引用 51 楼 sbwwkmyd 的回复:
楼主可以看看http://msdn.microsoft.com/zh-cn/magazine/cc164193.aspx中 复制和固定 那一节。 另外引用类型的堆栈地址也是可以获取到的,虽然单纯的通过纯值类型引用是不能获取到引用类型的地址(纯值类型与引用类型位于不同的堆栈),你可以定义一个结构体将值类型与引用类型一起绑定到引用类型堆栈
        [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit)]
        struct p
        {
            [System.Runtime.InteropServices.FieldOffset(0)]
            public int Int;
            [System.Runtime.InteropServices.FieldOffset(sizeof(int))]
            private object Value;
            public static p Get(object value)
            {
                return new p { Int = 10, Value = value };
            }
        }
比如32位目标程序,对于数组(string也一样),p1指向真实的数据,p1-1指向数组长度
            int[] array = new int[] { 9, 10 };
            p p = p.Get(array);
            int* p1 = (int*)*((&p.Int) + 1) + 2;
你要可靠的话,就只能传(&p.Int) + 1,但是并不是你要的int**,因为它和fixed的结果有2个int的偏差。
“虽然单纯的通过纯值类型引用是不能获取到引用类型的地址” 这句的意思是..? 我想获得栈上变量的地址信息,比如一个引用类型自身只占用4字节(32位系统),是在堆栈上开辟的,我想得到这个4字节内存的首地址。
回复 点赞
右右yy 2014年01月13日
引用 50 楼 Susiria 的回复:
就我来说,没有见过不用unsafe就能取得内存地址的方法,也没有framework内置方法(GCHandle的AddrOfPinnedObject只能取得固定对象的地址) string是很特殊的引用类型,字符串是任何程序都经常用到的类型,所以CLR对它有特殊的优化: “当CLR初始化时,它会创建一个内部的散列表,其中的键为字符串,值为指向托管堆中字符串的对象的引用...垃圾收集器不会释放CLR内部的散列表中引用的字符串对象,这些String对象的引用一直被散列表保存着。只有当进程中所有的应用程序域都不再引用这些字符串对象时,它们才会被释放。。。”(CLR via C#) 所以我认为,你说的不固定字符串也没有出错的情况,是因为这个字符串原本是在散列表之中的。
我在王涛的.net书中也看到关于字符串驻留特性,进程级别的,程序间可以共享,所以不由GC回收。但没有明确指明,GC会不会移动它..你引用的书似乎也没有说移动的事情。由于string怎么说也是在托管堆中存储的,我没找到确切资料前,不能排除顾虑,你有确切资料说"GC不会移动string"么?
回复 点赞
真相重于对错 2014年01月13日
托管与非托管的交互,注意两点, 1、二者运行在两个不同的地址空间。 2、托管有gc,也就是说.net框架要完全控制托管堆。 所以,托管传递给非托管参数,如果是这个参数托管的只能是复制,而不能是引用或地址,因为如果是地址的话,非托管就可以改变这个地址的内容,达到破坏gc的效果, 托管方只能传一个非托管的内存的地址或引用到非托管方 你可能会有疑问例如调用 时会有 ref int[] 之类的参数,其实这样写.net框架会自动把这个地址转换。
回复 点赞
右右yy 2014年01月13日
引用 49 楼 LargeSkyMensk 的回复:
第一个问题:你考虑到了.net的对象,随时会被移动。难道微软就考虑不到?而且无数的.net程序使用了很久出问题了吗?没有吧。要搞清楚这个问题,你需要知道.net在调用C++时候,如何处理托管内存与非托管内存的交互,两种方式:固定与复制。 第二个问题:使用ref表示表示传递指针。ref int表示:int *p,ref string 表示char **。当然使用ref最多只能传递我们在C++中所用的二级指针。有了一,二级指针,你的问题还解决不了吗? 那么,你需要更多级指针,更复杂的操作吗?使用marshal类手动处理内存的交互也是可以实现的。 建议,永远不要去用unsafe代码,或者 fixed功能。
有一二级指针自然够用,我的顾忌是:在传递引用类型前是否必须固定其地址。我的程序中前人写的代码,并没有固定地址的操作,直接传ref string给了C++,刚才有位仁兄说到string驻留机制特性是否是不固定地址也不会出错的原因。我也知string是进程级别的,不由GC回收。但我不清楚是否会由GC移动,因为string也是分配在托管堆上的。如果你能解答我这程序的写法是否正确,还是有隐患,在此感谢。 另外,你所说固定,却又不推荐fixed, 那就以GCHandle来进行固定?
回复 点赞
Code従業員 2014年01月13日
引用 39 楼 u012211533 的回复:
1.调用了,不调用我就不想搞明白C#->C++参数传递之法了。
这句话足够了,你的需求是制作外挂吧?从外挂的角度说,这个很难办到。 其中一个原因是长度无法判断,虽然是连续的,但是你并不知道他何时结束。 因为数据段和操作段不再一起,内存查找软件可以找出数据段地址,没法定位操作段。 将数组中的数单个取出来放到自己的数组中吧,外挂都是建立在耦合性上的。
回复 点赞
emailtome 2014年01月13日
发现大量半品水的C++er
回复 点赞
发动态
发帖子
C#
创建于2007-09-28

8.4w+

社区成员

64.0w+

社区内容

.NET技术 C#
社区公告
暂无公告