汇编代码的数据结构? [研究]

时间:2020-03-06 14:29:12  来源:igfitidea点击:

我打算创建一个优化的数据结构以容纳汇编代码。这样,我可以完全负责将在此结构上工作的优化算法。如果我可以在运行时进行编译。这将是一种动态执行。这可能吗?有人看到过这样的东西吗?

我是否应该使用结构将结构链接到程序流中。对象更好吗?

struct asm_code {
   int type;
   int value;
   int optimized;
   asm_code *next_to_execute;
 } asm_imp;

更新:我认为结果会像链表一样。

更新:我知道那里还有其他编译器。但这是一项军事最高机密项目。因此,我们不能信任任何代码。我们必须自己做。

更新:好的,我想我只会生成基本的i386机器代码。但是,完成后如何进入内存Blob?

解决方案

有可能的。动态代码生成在软件渲染和图形等某些领域甚至是主流。在各种脚本语言中,在机器代码(据我所知Perl的.NET,Java,动态编码字节码)中发现了许多用途,最近JavaScript也加入了这个俱乐部。

我们还会发现它也用在数学运算非常繁重的应用程序中,例如,如果我们打算进行数千次乘法运算,那么从矩阵乘法中删除所有具有零的乘法运算会有所不同。

我强烈建议我们阅读SSA的代码表示形式。这是一种表示形式,其中每个基元都变成了所谓的三个操作数形式,并且每个变量仅分配了一次(因此具有相同的"静态单一分配"形式)。

我们可以对此类代码进行高阶优化,并且很容易将其转换为可执行代码。不过,我们不会在周末编写该代码生成后端。

要了解SSA的外观,可以尝试使用LLVM编译器。在他们的网站上,他们有一个小"试用"小部件可以玩。我们将C代码粘贴到一个窗口中,我们会得到与SSA表单接近的内容。

小例子看起来像:

让我们在C中使用这个整数平方根算法。(任意示例,我只是简单而又不平凡):

unsigned int isqrt32 (unsigned int value) 
{
    unsigned int g = 0;
    unsigned int bshift = 15;
    unsigned int b = 1<<bshift;
    do {
        unsigned int temp = (g+g+b)<<bshift;
        if (value >= temp) {
            g += b;
            value -= temp;
        }
        b>>=1;
    } while (bshift--);
    return g;
}

LLVM将其转换为:

define i32 @isqrt32(i32 %value) nounwind  {
entry:
    br label %bb

bb:     ; preds = %bb, %entry
    %indvar = phi i32 [ 0, %entry ], [ %indvar.next, %bb ]      
    %b.0 = phi i32 [ 32768, %entry ], [ %tmp23, %bb ]
    %g.1 = phi i32 [ 0, %entry ], [ %g.0, %bb ]     
    %value_addr.1 = phi i32 [ %value, %entry ], [ %value_addr.0, %bb ]      
    %bshift.0 = sub i32 15, %indvar 
    %tmp5 = shl i32 %g.1, 1 
    %tmp7 = add i32 %tmp5, %b.0 
    %tmp9 = shl i32 %tmp7, %bshift.0    
    %tmp12 = icmp ult i32 %value_addr.1, %tmp9      
    %tmp17 = select i1 %tmp12, i32 0, i32 %b.0      
    %g.0 = add i32 %tmp17, %g.1     
    %tmp20 = select i1 %tmp12, i32 0, i32 %tmp9     
    %value_addr.0 = sub i32 %value_addr.1, %tmp20           
    %tmp23 = lshr i32 %b.0, 1       
    %indvar.next = add i32 %indvar, 1       
    %exitcond = icmp eq i32 %indvar.next, 16    
    br i1 %exitcond, label %bb30, label %bb

bb30:       ; preds = %bb
    ret i32 %g.0
}

我知道一开始看起来很恐怖。它甚至不是纯SSA形式。我们越阅读该表示形式,就会越有意义。我们还将发现为什么这种表示法在当今如此广泛的使用。

将我们需要的所有信息封装到数据结构中很容易。最后,我们必须决定是否要对操作码名称ect使用枚举或者字符串。

顺便说一句,我知道我没有给我们提供数据结构,而是给我们提供了一种正式但实用的语言,以及可以进一步查找的建议。

这是一个非常有趣的研究领域。

编辑:在我忘记之前:不要忽略.NET和Java的内置功能。这些语言使我们可以从程序中的字节码或者源代码进行编译并执行结果。

干杯,
尼尔斯

关于编辑:如何使用代码执行二进制Blob:

进入二进制二进制文件取决于操作系统和平台。简而言之,我们使指令高速缓存无效,也许我们必须写回数据高速缓存,并且可能必须在将代码写入到的内存区域中启用执行权限。

在win32上,这比较容易,因为如果将代码放在堆上,指令高速缓存刷新就足够了。

我们可以使用此存根开始:

typedef void (* voidfunc) (void);

void * generate_code (void)
{
    // reserve some space
    unsigned char * buffer = (unsigned char *) malloc (1024);

    // write a single RET-instruction
    buffer[0] = 0xc3; 

    return buffer;
}

int main (int argc, char **args)
{
    // generate some code:
    voidfunc func = (voidfunc) generate_code();

    // flush instruction cache:
    FlushInstructionCache(GetCurrentProcess(), func, 1024);

    // execute the code (it does nothing atm)
    func();

    // free memory and exit.
    free (func);
}

在99%的情况下,性能差异可以忽略不计。类的主要优点是,与过程代码相比,OOP生成的代码更好,更易于理解。

我不确定我们使用哪种语言编写代码,请注意在C语言中,类和结构之间的主要区别在于结构是值类型,而类是引用类型。在这种情况下,我们可能要从结构开始,但仍要向其添加行为(构造器,方法)。

在C ++代码中,不讨论优化代码的技术价值,而是在POD结构或者完整对象之间进行选择,这主要是封装的要点。

内联代码将使编译器优化(或者不优化)所使用的构造函数/访问器。不会有性能损失。

首先,设置一个构造函数

如果我们使用的是C ++编译器,请至少创建一个构造函数:

struct asm_code {
   asm_code()
      : type(0), value(0), optimized(0) {}

   asm_code(int type_, int value_, int optimized_)
      : type(type_), value(value_), optimized(_optimized) {}

   int type;
   int value;
   int optimized;
 };

至少,代码中不会包含未定义的结构。

数据的每种组合都有可能吗?

像使用struct这样使用结构意味着任何类型都是可能的,具有任何值和任何优化的。例如,如果我将type设置为25,值设置为1205且优化后的值设置为-500,那么就可以了。

如果我们不希望用户在结构中放入随机值,请添加内联访问器:

struct asm_code {

   int getType() { return type ; }
   void setType(int type_) { VERIFY_TYPE(type_) ; type = type_ ; }

   // Etc.

   private :
      int type;
      int value;
      int optimized;
 };

这将使我们可以控制结构内部设置的内容,并更轻松地调试代码(甚至可以对代码进行运行时验证)

我假设我们想要一个数据结构来保存某种指令模板,可能是从现有的机器代码中解析出来的,类似于:

add r1, r2, <int>

我们将拥有一个这种结构的数组,并将对该数组进行一些优化,可能会更改其大小或者构建一个新数组,并生成相应的机器代码。

如果目标计算机使用可变宽度的指令(例如x86),则无法确定数组大小,除非实际完成对从中构建阵列的指令的解析。另外,在从优化数组实际生成所有指令之前,我们无法确切确定需要多少缓冲区。但是我们可以做出很好的估计。

查看GNU Lightning。这可能对我们有用。