从C语言传递指针到汇编的方法

5

我想在我的C/C++程序中使用原子交换指令实现的"_test_and_set"汇编语言锁。

class LockImpl 
{
  public:
  static void lockResource(DWORD resourceLock )
  {
    __asm 
    {
      InUseLoop:  mov     eax, 0;0=In Use
                  xchg    eax, resourceLock
                  cmp     eax, 0
                  je      InUseLoop
    }

  }

  static void unLockResource(DWORD resourceLock )
  {
    __asm 
    {
      mov resourceLock , 1 
    }   

  }
};

这个可以工作,但是有一个bug。

问题在于我想传递DWORD * resourceLock而不是DWORD resourceLock。

所以问题是如何从C/C++传递指针到汇编语言并将其取回?

提前致谢。

敬礼, -Jay。

P.S. 这样做是为了避免在用户空间和内核空间之间进行上下文切换。


1
顺便提一下,汇编语言没有标准化。此外,汇编和C或C++之间的参数传递因编译器而异。因此,您需要提供编译器和平台标识(或查看我的答案以获取平台无关的过程)。 - Thomas Matthews
锁定资源(DWORD *resourceLock)的问题在哪里?即仅将resourceLock声明为指针,而在其他方面保持代码不变会有什么问题? - x4u
@gf:目前我的目标平台是Windows Server 2008,但最终会转向Monta Vista:Carrer Grade 5.0。因此,我目前正在使用Visual Studio 2008内置的编译器。 - Jay D
@John Knoeller:请阅读上面的评论以获取有关错误详细信息。谢谢。 - Jay D
@JayD:给我发送一封电子邮件到 Gmail,我会回复您测试工具中的可用代码。我的电子邮件用户名与此处相同。 - Adisak
显示剩余3条评论
7个回答

6
如果你在为Windows编写代码,你应该认真考虑使用临界区对象。临界区API函数经过优化,只有在真正需要时才会转换到内核模式,因此在没有争用的情况下,正常情况下几乎没有开销。
你自己实现的自旋锁最大的问题是,如果你在单CPU系统上等待锁定,那么你会使用所有可用的周期,并且持有锁定的任何内容都不会有机会运行,直到你的时间片结束并且内核抢占你的线程。
使用临界区比尝试自己编写用户模式自旋锁更成功。

我不想编写特定于Windows的代码。这就是为什么我没有使用Interlocked和其他东西的原因。我希望尽可能地使它通用。我迫切地等待Linux的移植,但至少在接下来的几个月内不会发生。 - Jay D
9
编写汇编代码无法提高可移植性。如果想要实现可移植的关键代码段,应该使用中立的API进行编码。可以考虑boost同步库:http://www.boost.org/doc/libs/1_41_0/doc/html/thread/synchronization.html - plinth
2
我投票反对这个简单的原因是它没有回答所问的问题。这个问题是一个有效的问题,回答它会很好看到。 - xcut
我会投赞成票,因为在一次只能运行一个线程的系统上自旋锁定是个糟糕的想法。反对这个建议就像是说:“我不在乎向下射击可能会伤到孩子,我只关心我要打中你放置的目标!”最后,我不明白为什么这个逻辑不能被抽象出来,这样无论你改变什么都只需要在一个小地方做一次即可。显然,当前计划的计划是这样做的,否则 OP 就不会使用 DWORD 和编写内联汇编了。 - San Jacinto
1
如果您知道您的平台具有多个硬件线程,编写自旋锁是可以的。默认的Window CS基本上是一个带有自旋计数的组合互斥锁+自旋锁。它尝试在回退到操作系统互斥锁之前自旋自旋计数次数。在单处理器系统上,使用的自旋计数始终为零,这使得默认行为只是互斥锁。 - Adisak

4

就您实际的问题而言,很简单:只需将函数头更改为使用volatile DWORD *resourceLock,并更改涉及resourceLock的汇编行以使用间接引用:

mov ecx, dword ptr [resourceLock]
xchg eax, dword ptr [ecx]

and

mov ecx, dword ptr [resourceLock]
lock mov dword ptr [ecx], 1

然而,请注意,您还有几个悬而未决的问题:
- 您表示正在Windows上进行开发,但希望切换到Linux。然而,您正在使用MSVC特定的内联汇编 - 当您移动到Linux时,这将不得不被移植到gcc风格(特别是需要从Intel语法切换到AT&T语法)。即使在Windows上,使用gcc也会使迁移的痛苦最小化(请参阅mingw以获取Windows上的gcc)。 - Greg Hewgill绝对正确,无用地旋转会阻止锁持有者获得CPU。如果您旋转时间过长,请考虑让出CPU。 - 在多处理器x86上,您可能会遇到围绕锁重排的内存加载和存储问题 - 锁定和解锁过程中的mfence指令可能是必要的。
实际上,如果您担心锁定,这意味着您正在使用线程,这很可能意味着您已经在使用平台特定的线程API。因此,请使用本机同步原语,并在切换到Linux时切换到pthread版本。

这对VC++内联汇编不起作用 - 至少在我的调试构建测试中是如此。实际上,您需要一个mov ebx,resourceLock,然后是一个xchg eax,[ebx]才能做到他想要做的事情。只有xchg eax,[resourceLock]实际上会将eax与指针(或引用地址)中存储的值交换。 - Adisak
2
解释:假设我们的指针有一个值(它的解引用内存也有一个值)- 假设 DWORD *resourceLock == 0x493004 并且 *(DWORD*)(0x00493004) == 1。然后在执行 mov ebx, resourceLock; xchg eax, [ebx] 后,eax 将等于 1。在执行 xchg eax, [resourceLock] 后,eax 将等于 0x493004,这绝对不是你想要的结果。 - Adisak
谢谢,已经修复了。我很久没有使用英特尔语法汇编了,有点生疏! - caf
@CAF:就你解锁代码中的“lock mov”而言,它只需要是一个“mov”。X86强制可见写入顺序,因此第二个锁是不必要的,实际上只会减慢速度。X86仅重新排序1)加载到加载和2)加载到存储到不同地址。没有存储重新排序,存储永远不会移动到加载之上。由于此代码使用了“xchg”(具有隐式锁定/ mfence),因此无需将mfence添加到代码中。在保持锁定时进行的任何修改都将在解锁写入可见之前可见。 - Adisak
Adisak:我担心的重新排序不是访问锁变量本身,而是由锁保护的共享数据的加载可能会被重新排序到关键部分之外。 - caf
显示剩余2条评论

3

显然,您正在使用内联汇编块在C++代码中使用MSVC进行编译。

一般而言,您应该真正使用编译器内置函数,因为内联汇编没有未来:当编译x64时,它不再被MS编译器支持。

如果您需要在汇编中对函数进行微调,则必须在单独的文件中实现它们。


内联汇编(即C/C++代码中的汇编块)不再受MS支持。就我所看到的,他的代码似乎是使用MSVC编译的内联汇编。 - Gregory Pakosz
@Ken,你喜欢这样改述吗? - Gregory Pakosz
@Ken,@Gregory:是的...没错。我讨厌Windows...但我的管理层认为它可以加快开发速度...WTF...我遇到了很多调度相关的问题,其中Win OS调度程序无法可靠地做出正确的事情,这使得一些线程变得狂野而其他线程则闲置不动...:(我尝试强制CPU亲和力,但那会严重影响性能... - Jay D
@Ken,使用给定的语法,内联汇编块只能是MS,这就是我最初回答的原因 :) - Gregory Pakosz

1
你应该使用类似这样的代码:
volatile LONG resourceLock = 1;

if(InterlockedCompareExchange(&resourceLock, 0, 1) == 1) {
    // success!
    // do something, and then
    resourceLock = 1;
} else {
    // failed, try again later
}

请参阅InterlockedCompareExchange


他的代码应该只使用InterlockedExchange()就能完美运行。对于未经检查的值交换,InterlockedExchange比InterlockedCompareExchange稍微快一些。 - Adisak
是的,但在这种情况下通常使用CAS,我想把他引向正确的方向。 - Fozi
哦,就我所知,内置的 InterlockedCompareExchange 和 CompareExchange 都会被简化成一条汇编指令,因此我不会说其中一个比另一个更快。好吧,CAS 可能需要多一个时钟周期。 - Fozi
实际上,CAS和XCHG有相当大的区别。首先,XCHG始终成功,而CAS可能会失败。CAS通常需要额外的代码来预读取值,然后如果它失败了,您必须进行重试或其他代码。尽管两者都是单个ASM指令,由于BusLock,它们都相当缓慢,但XCHG可以比CAS更快地执行,因为您必须始终检查CAS是否成功。如果您的算法足够简单以使用XCHG,则XCHG是首选操作。 - Adisak

1
问题在于原始版本需要使用寄存器间接寻址并使用引用(或指针参数)而不是按值传递锁DWORD。
下面是Visual C++的一个工作解决方案。编辑:我已经与作者离线合作,我们已经验证了此答案中的代码在其测试环境中正常工作。
但是,如果您正在使用Windows,则应该真正使用Interlocked API(即InterlockedExchange)。
编辑:如CAF所指出的,不需要lock xchg,因为xchg会自动断言BusLock。
我还添加了一个更快的版本,在尝试执行xchg之前进行非锁定读取。这显著减少了内存接口上的BusLock争用。在有争议的多线程情况下,通过为长时间保持的锁定进行回退(yield然后睡眠),可以更快地加速算法。对于单线程CPU情况,使用立即在被锁定时休眠的操作系统锁将是最快的。
class LockImpl
{
    // This is a simple SpinLock
    //  0 - in use / busy
    //  1 - free / available
public:
    static void lockResource(volatile DWORD &resourceLock )
    {
        __asm 
        {
            mov     ebx, resourceLock
InUseLoop:
            mov     eax, 0           ;0=In Use
            xchg    eax, [ebx]
            cmp     eax, 0
            je      InUseLoop
        }

    }

    static void lockResource_FasterVersion(DWORD &resourceLock )
    {
        __asm 
        {
            mov     ebx, resourceLock
InUseLoop:
            mov     eax, [ebx]    ;// Read without BusLock 
            cmp     eax, 0
            je      InUseLoop     ;// Retry Read if Busy

            mov     eax, 0
            xchg    eax, [ebx]    ;// XCHG with BusLock
            cmp     eax, 0
            je      InUseLoop     ;// Retry if Busy
        }
    }

    static void unLockResource(volatile DWORD &resourceLock)
    {
        __asm 
        {
            mov     ebx, resourceLock
            mov     [ebx], 1 
        }       

    }
};

// A little testing code here
volatile DWORD aaa=1;
void test()
{
 LockImpl::lockResource(aaa);
 LockImpl::unLockResource(aaa);
}

“xchg” 应该意味着总线锁定。但是,如果在 “unLockResource” 中的 “mov” 指令上没有加上 “lock” 前缀,那么我认为您需要在其之前添加一个 “mfence” 指令,以确保您刚刚离开的临界区域中的所有副作用都被其他处理器看到,然后才允许它们进入它们的临界区域。 - caf
你验证过使用这个锁的程序是否与其他类型的锁一起工作了吗?我在一个高度争议的测试平台上运行了上面的代码,该平台进行多线程修改和独立验证,使用8个线程进行锁定。这个测试平台足够强大,它发现了MSDN网站上常见的Ruediger-Asche-ReaderWriterLock中的一个错误。尽管这段代码比我实际使用的锁定机制慢几个数量级,但它通过了“正确性”功能的测试。可能是你的调用代码不正确? - Adisak
lockTestThreadStatus_ = 1; while(lockTestThreadStatus_ == 1) { LockImpl::lockResource(commonResource->lockCnt); //===========关键区块 === commonResource->resource++; printf("线程 ID :%ld 资源数值:%ld \n", GetCurrentThreadId(), commonResource->resource); //===========关键区块结束==== LockImpl::unLockResource(commonResource->lockCnt); } - Jay D
这是我的调用代码...基本上这是线程函数。我希望看到多个线程ID依次增加此资源计数器。但是使用以上代码,我只能看到一个线程ID持续地执行自增操作。具有讽刺意味的是,我发布的代码显示了正确的行为,尽管我知道其中的错误。 :) 此测试在没有超线程的双核机器上运行。 - Jay D
@Adisak 好的,即使在我运行测试的电脑上它有些问题,我仍然接受您的答案。我会在其他电脑上尝试一下。 - Jay D
显示剩余11条评论

0

查看您的编译器文档,了解如何打印函数生成的汇编语言。

打印此函数的汇编语言:

static void unLockResource(DWORD resourceLock )
{
  resourceLock = 0;
  return;
}

这可能无法正常工作,因为编译器可以优化函数并删除所有代码。您应该更改上述函数以传递指向resourceLock的指针,然后使函数设置锁定。打印此工作函数的汇编。


-1

我已经提供了一个有效的版本,回答了原帖作者有关如何在ASM中传递参数以及如何正确使用锁的问题。

许多其他答案都质疑使用ASM的明智性,并提到应该使用内部函数或C操作系统调用。以下也同样有效,是我ASM答案的C++版本。其中有一小段ASM代码,只需要在您的平台不支持InterlockedExchange()时使用。

class LockImpl
{
    // This is a simple SpinLock
    //  0 - in use / busy
    //  1 - free / available
public:
#if 1
    static DWORD MyInterlockedExchange(volatile DWORD *variable,DWORD newval)
    {
        // InterlockedExchange() uses LONG / He wants to use DWORD
        return((DWORD)InterlockedExchange(
            (volatile LONG *)variable,(LONG)newval));
    }
#else
    // You can use this if you don't have InterlockedExchange()
    // on your platform. Otherwise no ASM is required.
    static DWORD MyInterlockedExchange(volatile DWORD *variable,DWORD newval)
    {
        DWORD old;
        __asm 
        {
            mov     ebx, variable
            mov     eax, newval
            xchg    eax, [ebx]  ;// XCHG with BusLock
            mov     old, eax
        }
        return(old);
    }
#endif
    static void lockResource(volatile DWORD &resourceLock )
    {
        DWORD oldval;
        do 
        {
            while(0==resourceLock)
            {
                // Could have a yield, spin count, exponential 
                // backoff, OS CS fallback, etc. here
            }
            oldval=MyInterlockedExchange(&resourceLock,0);
        } while (0==oldval);
    }
    static void unLockResource(volatile DWORD &resourceLock)
    {
        // _ReadWriteBarrier() is a VC++ intrinsic that generates
        // no instructions / only prevents compiler reordering.
        // GCC uses __sync_synchronize() or __asm__ ( :::"memory" )
        _ReadWriteBarrier();
        resourceLock=1;
    }
};

问题不在于是否使用内联汇编,而是如何从C语言中通过引用将变量传递到汇编中,而不是按值传递。感谢您提供的扩展代码! - Jay D
哇...考虑到最佳答案只是说“使用临界区”而根本没有回答问题,这里被踩似乎有点奇怪。特别是当许多其他答案都在抱怨不使用ASM,而这不仅是一个后续,而且是一个实际的工作示例,可以解决这些回应。我实际上花了一些时间和精力,联系作者离线帮助他在自己的代码测试平台上使事情正常运行,然后还提供了另一个非ASM工作版本。我想这样做并不能得到好处。 - Adisak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接