跨平台VM的C内存管理

Naw*_*Man 6 c memory-management cross-platform low-level vm-implementation

我问一个问题,关于C型尺寸,我得到一个很好的答案,但我意识到,我可能不会制定这个问题非常好是我的目的.

我的背景来自计算机工程师,然后转到软件工程师,所以我喜欢计算机架构,并且总是考虑制作VM.我刚刚完成了一个有趣的Java项目,我很自豪.但是有些法律问题我现在无法开源,而且我现在有空闲时间.所以我想看看我是否可以在C上制作另一个VM(速度更快),只是为了娱乐和教育.

事情是,我上次写一篇非琐事C问题时,我不是一个C程序,这是10多年前的事了.我是Pascal,Delphi,现在是Java和PHP程序员.

我可以预见有很多障碍,我正试图解决一个问题,那就是访问现有的库(在Java中,反射解决了这个问题).

我计划通过拥有一个数据缓冲区(类似于堆栈)来解决这个问题.我的VM客户端可以编程将数据放入这些堆栈,然后再指向本机函数.

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;
}
Run Code Online (Sandbox Code Playgroud)

本机函数的推送,拉取和调用可以由字节代码(即稍后将如何创建VM)触发.

为了完整起见(这样你可以在你的机器上试试),这里是Stack的代码:

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));
}
Run Code Online (Sandbox Code Playgroud)

在本机功能方面,我为测试目的创建了以下内容:

// 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
Run Code Online (Sandbox Code Playgroud)

这在我的机器上运行良好(Linux x86 32位GCC-C99).如果这也适用于其他OS/Architecture,那将是非常好的.但至少有三个与记忆相关的问题我们必须要注意.

1).数据大小 - 似乎如果我在同一架构上使用相同的编译器编译VM和本机函数,则大小类型应该相同.

2).字节顺序 - 与数据大小相同.

3).内存对齐 - 这是问题,因为padding-bytes可能会在struct中添加,但是在准备参数堆栈时很难同步它(除了硬编码之外,无法知道如何添加填充).

我的问题是:

1).如果我知道类型的大小,有没有办法修改推拉函数以与struct padding完全同步?(修改让编译器像Datasize和Endians问题一样处理它).

2).如果我把结构打包(使用#pragma pack(1)); (2.1)性能损失是否可以接受?(2.2)程序稳定性是否会有风险?

3).填充2,4或8怎么样?对于一般的32位或64位系统哪个应该好?

4).你能指导我一个准确的填充算法的文档,比如x86上的GCC吗?

5).有更好的方法吗?

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

对不起我的英文和很长的帖子.

提前谢谢大家.

Jon*_*ler 2

离题评论

这些第一条与您提出的问题无关,但是......

// Execute
void (*NativeFunction)(char*, char*) = &Plus;
NativeFunction(Params, Result); // Call the function
Run Code Online (Sandbox Code Playgroud)

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

typedef void (*Operator)(void *params, void *result);
Run Code Online (Sandbox Code Playgroud)

然后你可以写:

Operator NativeFunction = Plus;
Run Code Online (Sandbox Code Playgroud)

实际的功能也会被修改 - 但只有很小的修改:

void Plus(void *pParams, void *pResult)
Run Code Online (Sandbox Code Playgroud)

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


直接回答问题

1)。如果我知道类型的大小,有没有办法修改推和拉函数以与结构填充完全同步?(修改为让编译器像 Datasize 和 Endians 问题一样处理它)。

没有一个简单的方法可以做到这一点。例如,考虑:

struct Type1
{
     unsigned char byte;
     int           number;
};
struct Type2
{
     unsigned char byte;
     double        number;
};
Run Code Online (Sandbox Code Playgroud)

在某些体系结构(例如 32 位或 64 位 SPARC)上,Type1 结构将在 4 字节边界上对齐“数字”,但 Type2 结构将在 8 字节边界上对齐“数字”(并且可能在 16 字节边界上有一个“long double”)。您的“推送单个元素”策略会在推送“字节”值后将堆栈指针增加 1 - 因此,如果堆栈指针尚未正确,您可能希望在推送“数字”之前将堆栈指针移动 3 或 7对齐。VM 描述的一部分将是任何给定类型所需的对齐;相应的推送代码在推送之前需要确保正确对齐。

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?

您必须阅读手册

5)。有更好的办法吗?

请注意,带有单个字符后跟类型的“Type1”技巧为您提供了对齐要求 - 可能使用以下中的“offsetof()”宏<stddef.h>

offsetof(struct Type1, number)
Run Code Online (Sandbox Code Playgroud)

好吧,我不会将数据打包在堆栈上 - 我会使用本机对齐,因为这设置为提供最佳性能。编译器编写者不会随意向结构添加填充;他们把它放在那里是因为它对架构来说“最好”。如果您认为自己了解得更多,则可以预见通常的后果 - 速度较慢的程序有时会失败并且不那么可移植。

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