代码之家  ›  专栏  ›  技术社区  ›  NawaMan

跨平台虚拟机的C内存管理

  •  6
  • NawaMan  · 技术社区  · 15 年前

    我问了一个 question 关于C型尺寸,我得到了一个很好的答案,但我意识到我可能无法很好地阐述问题,以便对我的目的有用。

    我的背景是从计算机工程师到软件工程师,所以我喜欢计算机体系结构,总是在考虑制造虚拟机。我刚刚完成了一个有趣的项目,用Java制作VM,我很自豪。但是有一些法律问题我现在不能开源,我现在有一些空闲时间。所以我想看看我是否能在C上制作另一个虚拟机(速度更快),只是为了好玩和教育。

    问题是我不是一个C程序,我上一次写一个非琐碎的C问题是在10多年前。我是Pascal,Delphi,现在是Java和PHP程序员。

    有许多障碍我可以预见,我正在尝试解决一个问题,那就是访问现有的图书馆(在爪哇,反射解决了这个问题)。

    我计划通过拥有数据缓冲区(类似于堆栈)来解决这个问题。我的虚拟机的客户机可以在给我提供指向本机函数的指针之前编程将数据放入这些堆栈中。

    int main(void) {
        // Prepare stack
        int   aStackSize = 1024*4;
        char *aStackData = malloc(aStackSize);
    
        // Initialise stack
        VMStack aStack;
        VMStack_Initialize(&aStack, (char *)aStackData, aStackSize);
    
        // Push in the parameters
        char *Params = VMStack_CurrentPointer(&aStack);
        VMStack_Push_int   (&aStack, 10  ); // Push an int
        VMStack_Push_double(&aStack, 15.3); // Push a double
    
        // Prepare space for the expected return
        char *Result = VMStack_CurrentPointer(&aStack);
        VMStack_Push_double(&aStack, 0.0); // Push an empty double for result
    
        // Execute
        void (*NativeFunction)(char*, char*) = &Plus;
        NativeFunction(Params, Result); // Call the function
    
        // Show the result
        double ResultValue = VMStack_Pull_double(&aStack); // Get the result
        printf("Result:  %5.2f\n", ResultValue);               // Print the result
    
        // Remove the previous parameters
        VMStack_Pull_double(&aStack); // Pull to clear space of the parameter
        VMStack_Pull_int   (&aStack); // Pull to clear space of the parameter
    
        // Just to be sure, print out the pointer and see if it is `0`
        printf("Pointer: %d\n", aStack.Pointer);
    
        free(aStackData);
        return EXIT_SUCCESS;
    }

    本地函数的推、拉和调用可以由一个字节代码触发(这就是稍后生成VM的方法)。

    为了完整性(以便您可以在计算机上进行尝试),下面是用于堆栈的代码:

    typedef struct {
        int  Pointer;
        int  Size;
        char *Data;
    } VMStack;
    
    inline void   VMStack_Initialize(VMStack *pStack, char *pData, int pSize) __attribute__((always_inline));
    inline char   *VMStack_CurrentPointer(VMStack *pStack)                    __attribute__((always_inline));
    inline void   VMStack_Push_int(VMStack *pStack, int pData)                __attribute__((always_inline));
    inline void   VMStack_Push_double(VMStack *pStack, double pData)          __attribute__((always_inline));
    inline int    VMStack_Pull_int(VMStack *pStack)                           __attribute__((always_inline));
    inline double VMStack_Pull_double(VMStack *pStack)                        __attribute__((always_inline));
    
    inline void VMStack_Initialize(VMStack *pStack, char *pData, int pSize) {
        pStack->Pointer = 0;
        pStack->Data    = pData;
        pStack->Size    = pSize;
    }
    
    inline char *VMStack_CurrentPointer(VMStack *pStack) {
        return (char *)(pStack->Pointer + pStack->Data);
    }
    
    inline void VMStack_Push_int(VMStack *pStack, int pData) {
        *(int *)(pStack->Data + pStack->Pointer) = pData;
        pStack->Pointer += sizeof pData; // Should check the overflow
    }
    inline void VMStack_Push_double(VMStack *pStack, double pData) {
        *(double *)(pStack->Data + pStack->Pointer) = pData;
        pStack->Pointer += sizeof pData; // Should check the overflow
    }
    
    inline int VMStack_Pull_int(VMStack *pStack) {
        pStack->Pointer -= sizeof(int);// Should check the underflow
        return *((int *)(pStack->Data + pStack->Pointer));
    }
    inline double VMStack_Pull_double(VMStack *pStack) {
        pStack->Pointer -= sizeof(double);// Should check the underflow
        return *((double *)(pStack->Data + pStack->Pointer));
    }

    在本机函数方面,我创建了以下内容用于测试:

    // These two structures are there so that Plus will not need to access its parameter using
    //    arithmetic-pointer operation (to reduce mistake and hopefully for better speed).
    typedef struct {
        int    A;
        double B;
    } Data;
    typedef struct {
        double D;
    } DDouble;
    
    

    // Here is a helper function for displaying void PrintData(Data *pData, DDouble *pResult) { printf("%5.2f + %5.2f = %5.2f\n", pData->A*1.0, pData->B, pResult->D); }

    // Some native function void Plus(char* pParams, char* pResult) { Data *D = (Data *)pParams; // Access data without arithmetic-pointer operation DDouble *DD = (DDouble *)pResult; // Same for return DD->D = D->A + D->B; PrintData(D, DD); }

    执行时,上述代码返回:

    10.00 + 15.30 = 25.30
    Result:  25.30
    Pointer: 0

    这在我的机器(Linuxx86 32位GCC-C99)上工作得很好。如果在其他操作系统/体系结构上也能工作,那将是非常好的。但是至少有三个与内存相关的问题我们必须注意。

    1)。数据大小-如果我在相同的体系结构上使用相同的编译器编译VM和本机函数,那么大小类型应该是相同的。

    2)。endianness-与数据大小相同。

    3)。内存对齐-这是一个问题,因为填充字节可以添加到结构中,但是在准备参数堆栈as时很难对其进行同步(除了硬编码之外,没有办法知道如何添加填充)。

    我的问题是:

    1)。如果我知道类型的大小,有没有方法修改push和pull函数以与结构填充完全同步?(修改以让编译器处理数据大小和endian问题等问题)。

    2)。如果我一个一个地包装结构(使用 #pragma pack(1) )(2.1)性能罚款是否可以接受?(2.2)项目的稳定性是否有风险?

    3)。填充2、4或8怎么样?对于一般的32位或64位系统,哪一个比较好?

    4)。你能给我介绍一个关于精确填充算法的文档吗,比如说x86上的gcc?

    5)。有更好的方法吗?

    注意:跨平台不是我的最终目标,但我无法抗拒。而且,只要表现不那么难看,就不是我的目标。所有这些都是为了娱乐和学习。

    抱歉我的英语和很长的帖子。

    提前感谢大家。

    3 回复  |  直到 9 年前
        1
  •  2
  •   Community noseratio    7 年前

    切向注释

    这些第一项与你提出的问题无关,但是…

    // Execute
    void (*NativeFunction)(char*, char*) = &Plus;
    NativeFunction(Params, Result); // Call the function
    

    我认为你应该使用“void*”而不是“char*”。我还有一个typedef用于函数指针类型:

    typedef void (*Operator)(void *params, void *result);
    

    然后你可以写:

    Operator NativeFunction = Plus;
    

    实际的函数也会被修改-但只是非常轻微的:

    void Plus(void *pParams, void *pResult)
    

    另外,您还有一个小的命名问题-此函数是“intplusDoubleGivesDouble()”,而不是一个通用的“添加任意两种类型”函数。


    直接回答问题

    1)。如果我知道类型的大小,有没有方法修改push和pull函数以与结构填充完全同步?(修改以让编译器处理数据大小和endian问题等问题)。

    要做到这一点并不容易。例如,考虑:

    struct Type1
    {
         unsigned char byte;
         int           number;
    };
    struct Type2
    {
         unsigned char byte;
         double        number;
    };
    

    在某些体系结构(例如32位或64位SPARC)上,type1结构将在4字节边界上对齐“number”,但type2结构将在8字节边界上对齐“number”(并且可能在16字节边界上具有“long double”)。如果堆栈指针尚未正确对齐,则在推送“byte”值后,“push individual elements”策略会将堆栈指针移动1,因此在推送“number”之前,您需要将堆栈指针移动3或7。虚拟机描述的一部分将是任何给定类型所需的对齐;相应的推送代码需要在推送之前确保正确对齐。

    2)。如果我一个一个地打包结构(使用pragma pack(1));(2.1),性能惩罚是否可以接受?(2.2)项目的稳定性是否有风险?

    在x86和x86_64计算机上,如果打包数据,则会因数据访问不对齐而导致性能损失。在SPARC等机器上 或PowerPC (每) mecki ,您将得到一个总线错误或类似的结果—您必须以正确的对齐方式访问数据。您可能会节省一些内存空间—这会牺牲性能。您最好以空间的边际成本来确保性能(这里包括“正确执行而不是崩溃”)。

    3)。填充2、4或8怎么样?对于一般的32位或64位系统,哪一个比较好?

    在SPARC上,您需要将一个n字节的基本类型填充到一个n字节的边界上。在x86上,如果您这样做,您将获得最佳性能。

    4)。你能给我介绍一个关于精确填充算法的文档吗?比如说在x86上的gcc?

    你必须阅读 manual .

    5)。有更好的方法吗?

    请注意,“type1”技巧只需一个字符后跟一个类型,就可以满足对齐要求-可能需要使用“offsetof()”宏 <stddef.h> :

    offsetof(struct Type1, number)
    

    好吧,我不会将数据打包到堆栈上——我会使用本机对齐,因为它被设置为提供最佳性能。编译器编写器不会随意向结构添加填充;他们将其放在那里是因为它对体系结构“最有效”。如果你决定更好地了解,你可以期待通常的结果-较慢的程序,有时会失败,而且不太可移植。

    我也不相信我会在操作符函数中编写代码来假设堆栈包含一个结构。我将通过params参数从堆栈中提取值,知道正确的偏移量和类型是什么。如果我推了一个整数和一个双精度数,那么我会拉一个整数和一个双精度数(或者,可能,按相反的顺序-我会拉一个双精度数和一个整数)。除非您计划一个异常的虚拟机,否则很少有函数会有很多参数。

        2
  •  1
  •   dirkgently    15 年前

    有趣的帖子,显示你投入了大量的工作。几乎是理想的。

    我没有现成的答案,请耐心等待。我还要问几个问题:p

    1)。如果我知道类型的大小,有没有方法修改push和pull函数以与结构填充完全同步?(修改以让编译器处理数据大小和endian问题等问题)。

    这只是从性能的角度来看吗?您计划引入指针和本机算术类型吗?

    2)。如果我一个一个地打包结构(使用pragma pack(1));(2.1),性能惩罚是否可以接受?(2.2)项目的稳定性是否有风险?

    这是一个实现定义的东西。不是你能指望的跨平台的东西。

    3)。填充2、4或8怎么样?对于一般的32位或64位系统,哪一个比较好?

    与本机单词大小匹配的值应该能给您提供最佳的性能。

    4)。你能给我介绍一个关于精确填充算法的文档吗,比如说x86上的gcc?

    我不知道我的头上有什么。但我见过类似的代码 this 被使用。

    注意,你可以 specify attributes of variables 使用gcc default_struct __attribute__((packed)) 关闭填充)。

        3
  •  1
  •   Nicholas Jordan    15 年前

    这里有一些非常好的问题,其中许多将与一些重要的设计问题纠缠在一起,但对我们大多数人来说,我们可以看到你正在努力的方向(在我写作时直接发布,这样你就可以看到你正在产生兴趣),我们可以很好地理解你的英语,你正在努力的方向是一些编译器问题,以及一些语言设计问题-很难解决这个问题,但是您已经在JNI工作了,这是希望…

    首先,我会努力摆脱语用学;很多人, 非常多 会不同意的。关于原因的规范化讨论,请参阅D语言在这个问题上的立场的理由。另外,代码中还隐藏了一个16位指针。

    这些问题几乎无穷无尽,研究得很好,而且很可能使我们陷入反对和内部不妥协的境地。如果我可以建议你阅读 Kenneth Louden's Home Page 以及英特尔体系结构手册。我已经读过了。数据结构的一致性,以及您提出讨论的许多其他问题都深深地隐藏在历史编译器科学中,并且很可能会让您沉浸在谁知道什么。(俚语或成语,指不可预见的后果)

    上面写着:

    1. C型尺寸 什么型号的?
    2. 移动前的计算机工程师 软件工程师 学过微控制器吗?看看唐·兰开斯特的一些作品。
    3. Pascal,Delphi,现在的Java和PHP 程序员。 虽然许多人会展示或试图展示如何使用它们来编写强大的基本例程,但它们与处理器的基础基础基础架构相比,已经被删除了。我建议看一下DavidEck的递归下降解析器,看看如何开始研究这个问题。同时,KennethLouden也有一个“微小”的实现,它是一个实际的编译器。不久前我发现了一个我认为叫做asm-dot-org的东西…在那里可以学习到非常先进、非常强大的工作,但是要想进入编译器科学,用汇编程序开始写作是一个漫长的过程。此外,大多数体系结构在不同处理器之间存在不一致的差异。
    4. 访问现有库

    有很多LIBS,Java有一些好的。我不了解其他人。一种方法是尝试编写lib。Java有一个良好的基础,为人们喜欢尝试更好的东西留出空间。从改善Knuth-Morris-Pratt或其他方面开始:开始的地方并不短缺。尝试 Computer Programming Algorithms Directory 当然,看看 Dictionary of Algorithms and Data Structures 在NIST

    1. 一直在排队

    不一定,请参阅Dov Bulka-该员工拥有CS博士学位,并且在时间效率/可靠性稳健性等领域是一位精通的作者,这些领域不受某些“业务模型”范式的约束,因此我们可以从中获得一些“噢!在那些真正重要的问题上,这无关紧要。

    作为一个总结,如您所描述的,仪表和控制占完成编程技能的实际市场的60%以上。出于某种原因,我们主要听说了商业模式。让我和你分享我从可靠的来源得到的内幕消息。从 10%到60%或更多 实际的安全和财产风险来自车辆问题,而不是来自盗窃、盗窃和类似的事情。你将永远听不到“在县矿产开采厂工作90天”的呼吁!对于交通罚单,事实上大多数人甚至没有意识到交通引用是(美国)4级轻罪,实际上是可以归类的。

    在我看来,你已经朝着一些好的工作迈出了一大步……