迷失多线程:不用汇编模拟实现简单可控多线程

之前提供了一个在BREW等Arm和X86上的模拟多线程,核心部分是用汇编写的,里面的关键是栈的切换。今天偶尔清闲一下,突然想能不能不用汇编来实现,有些人一听汇编就头大了。于是就用了一下午来研究下如何操作。代码并不多,只是因为我在linux下写的,只能使用gdb来调试,而我对gdb非常不熟悉,所以弄起来非常的慢,大部分的时间都是靠仔细的分析代码来找错误的原因和地方的,浪费了不少精力,期间还在白纸上来描述流程,写部分实现,分析。就这些年的集中精力来说,可以说也花费了不少脑细胞了。

闲话不说了,苦也受过了,先看代码:

       #include "stdio.h"
#include "stdlib.h"
class Runable
{
public:
        virtual ~Runable()
                {
                
        }
        virtual void run() = 0;
        virtual void resume() = 0;
        virtual void suspend() = 0;
        virtual void start() = 0;
        virtual bool isexecute() = 0;
};


class Thread
{
public:
        Thread(){
                
        }

        virtual ~Thread(){
                
        }
        virtual void run() = 0;
        virtual void start() {
                if (isExecute()) return;
                _pStack = (size_t*)(&_stack[1024-4]);
                StartThread(this);
                return;
        }
        virtual void suspend()
                {
                        if (isSuspend() || !isExecute()) return;
                        _bSuspend = true;
                        SwitchThread(this);
                }
        virtual void resume()
                {
                        if (!isSuspend()) return;
                        _bSuspend = false;
                        SwitchThread(this);
                }
        virtual bool isExecute()
                {
                        return (_bExecute != 0);
                }
        virtual bool isSuspend()
                {
                        return _bSuspend;
                }
        static void end(Thread* pThread)
                {
                        register size_t* ret = (size_t*)(&pThread- 2);
                        register size_t bak = *(pThread->_pStack);
                        *ret = bak;
                }

        static void StartCallback(Thread* pThread)
                {
                        pThread->run();
                        pThread->_bExecute = 0;
                        end(pThread);
                }
        static void StartThread(Thread* pthis)
                {
                        pthis->_bExecute = 1;
                        register size_t* ret = (size_t*)(&pthis-2);
                        register size_t bak = *(pthis->_pStack);
                        *(pthis->_pStack) = *ret;
                        StartCallback(pthis);
                }
        static void SwitchThread(Thread* pthis)
                {
                        register size_t* ret = (size_t*)(&pthis- 2);
                        register size_t bak = *(pthis->_pStack);
                        *(pthis->_pStack) = *ret;
                        *ret = bak;
                }
        size_t* _pStack;
        int _bExecute;
        char _stack[1024];
        bool _bSuspend;

};
class test_thread: public Thread
{
public:
        test_thread()
                {
                        
                }
        virtual ~test_thread()
                {
                        
                }
        virtual void run()
                {
                        int k = 0;
                        while (k < 100) {
                                printf("k=%d\n", k++);
                                suspend();
                        }
                 
                }
};


int main(int argc, char *argv[])
{
        test_thread* test = new test_thread();
        int j = 0;
        while (j < 1000) {
                printf("j = %d\n", j++);
                if (!test->isExecute()) {
                        test->start();
                }
                else {
                        test->resume();
                }
        }
        delete test;
        return 0;
}
       

抱歉没有注释,实在来不及写了。其实外部的接口和上次的完全一样,不一样的仅在于实现而已。

现在来进行技术说明,线程的关键是切换栈指针,及汇编里面的esp寄存器,用汇编无疑是最方便的,但C/C++的话也是可以的,如果内嵌汇编的话,我也不说了。修改的办法就是修改函数的返回地址。可能有些人比较蒙,先看下汇编的函数调用:

       push 5
       call fun
       add esp, 4; 或者pop eax或其他等效的
       

上述的代码实现的就是调用fun(5)的汇编实现,而在栈内存中,呈如下排列:

5

ret_address

凑活着看吧,函数返回地址ret_address(调用函数语句的下一条语句)和变量5的地址的关系如下:

ret_address = address(5) – 4

我们在C/C++中其实可以调用&(5)来获得参数5的内存地址,而这个地址-4就是返回地址。现在已经很明了了,就是要巧妙的构造函数返回地址。

不过在C++的类中有些不太一样,就是在调用类函数的时候,默认会传递一个类的指针。所以在这里就将pthis/pThread的地址-2了(指针地址都是32位的,所以-2,相当于字节-8)。

剩下的就没有太多可说的了,在start函数中,设置自定义的栈顶(栈由高往低走)。然后调用StartThread,在StartThread中重新设置函数返回地址,然后调用StartCallback函数,StartCallback函数将会调用线程的thread::run函数。在一般情况下,run函数中最会用一个suspend,在suspend中最终会调用SwitchThread函数来重新设置返回值,使之返回到外部的主函数中。而StartCallback中的pThread->run()之后的语句,只有在Thread::run正常返回时(run函数的结束)才会执行,这样来完成一个流程。但这样有一个问题,由于我们不是直接修改esp值,我们其实是通过ebp值来修改esp值(我更乐意称这种行为是影响,而不是修改),这样以来就要保证栈平衡,即有多少调用函数就要有多少个返回(其实汇编里面更通常的说法是有多少个push就要有多少个pop),由于在start函数中多进行了一次函数调用(外部函数->start->StartThread),因此,返回的流程就是(StartCallback->end->外部函数),这样来实现栈平衡的。因此就有了下面的问题:

在使用的时候,必须严格控制调用的层次,在上次的线程模拟中,使用下面的方法:

       class test_thread:public Thread
       {
       //....
       void OnRun()
       {
       if (!isExecute) {
       start();
       }
       else {
       resume();
       }
       }
       //....
       };
       

上述语句在这次的调用中将会失败,原因在于多了一层,而上次的可以是由于我们直接修改esp值,更改了函数的调用(我有点怀疑上次的线程在机器上运行不稳定是否是这个原因)。这个就是使用高级语言实现的代价。在C库中,有一个setjmp的函数,可以保存寄存器中的值,我没有仔细研究那个函数,因此对于能否结合这次的程序,达到完美的不用汇编实现的多线程不太清楚。

我没有看过别的线程实现代码,比如pthread的,对于它们的实现方式和手段也不清楚。本文仅仅从研究的角度出发。

PS:上述代码在ubuntu 10.10 x86_32bit上测试通过,并未在其他系统上进行测试。

PS2:上述代码由于我对gdb使用不熟悉,分析过程大都依靠语句分析,并未结合实际状况,因此,如有错误,还请谅解。

发布者

rix

如果连自己都不爱自己,哪还有谁来爱你