Delphi源代码分析读书笔记
Delphi源代码分析读书笔记

第1章 最小化Delphi内核

第2章 基本数据类型的实现

  • 结构体变体类型,基本上没用过这种类型,以前对这个语法理解的迷迷糊糊的
TPerson = record
    FirstName, LastName: string[40];
    BirthDate: TDate;
    case Citizen: Boolean of
        True:
        (Birthplace: string[40]);
        False:
        (Country: string[20];
            EntryPort: string[20];
            EntryDate, ExitDate: TDate);
    end;

其中的Citizen相当于另外添加了一个Boolean字段,等价于

TPerson = record
    FirstName, LastName: string[40];
    BirthDate: TDate;
    Citizen: Boolean;
    case Boolean of
        True:
        (Birthplace: string[40]);
        False:
        (Country: string[20];
            EntryPort: string[20];
            EntryDate, ExitDate: TDate);
end;

可以在代码中使用Citizen,也可以不使用。Citizen完完全全的相当于一个独立字段,Delphi并不会自动关联或检查Citizen的值和实际存储的值,即是说可以给Citizen赋值为False, 同时给Birthplace赋值,或者给Citizen赋值为True,同时给Country、EntryPort等变量赋值。也可以只给Birthplace赋值,而完全不处理Citizen。这都是取决于程序的需要。当然在一般情况下,两者需要保持关联,赋值时保持一致,读取时根据类型来决定读哪些字段。实际第二段代码里的True、False是没有意义的,对程序员和编译器来说都没意义。第一段代码里的True、False对编译器来说也没有任何意义,但可以方便程序员理解代码。
那怎么判断变体部分到底存的什么内容呢?简单来说无法判断。但是在实际使用过程中,可以根据上下文来确定取值。或者在有tag字段的时候根据tag值来判断,当然这只能依赖于写值时正确的保持了关联。

Int64Rec = packed record
    case Integer of
        0: (Lo, Hi: Cardinal);
        1: (Cardinals: array [0..1] of Cardinal);
        2: (Words: array [0..3] of Word);
        3: (Bytes: array [0..7] of Byte);
end;
function FileSeek(Handle: THandle; const Offset: Int64; Origin: Integer): Int64;
{$IFDEF MSWINDOWS}
begin
    Result := Offset;
    Int64Rec(Result).Lo := SetFilePointer(Handle, Int64Rec(Result).Lo,
        @Int64Rec(Result).Hi, Origin);
    if (Int64Rec(Result).Lo = $FFFFFFFF) and (GetLastError <> 0) then
        Int64Rec(Result).Hi := $FFFFFFFF;
end;

Int64Rec是在SysUtils中定义的类型,这个类型实际上提供了一些方便的方法来操作Int64变量的各个字节。Int64一共是8个字节,这8个字节分别可以按照8个Byte,4个Word,2个Cardinal来访问。Int64Rec主要是用于类型转换,因为在类型转换时可能需要单独分析各个字节的值。
FileSeek的参数Offset是表示文件位置的偏移量,在Delphi中定义为Int64,但在Windows API中需要两个32位整数,低32位和高32位分开传参(这可能和早期C语言中没有Int64的类型有关?)。所以后面调用API时就转成Int64Rec,然后用Lo、Hi分别访问低32位和高32位。
这个例子也解释了为什么可以不加tag,这里的用法实际上不是根据不同情况来保存不同的值,相反,这里保存的值一定是Int64,只是对Int64可以有多种不同的解释和使用方式。
注意这里面的0、1、2、3实际是没有任何意义的,而且实际上这里还不能加tag,因为加了tag后相当于增加了一个字段,就和Int64结构不一致了。

  • 全局变量在应用程序的数据区分配,而局部变量在应用程序的栈上分配。因此,相对于定义的顺序,多个全局变量的地址是递增的,而局部变量递减。此外,由于每次调用函数时,栈顶可能发生变化,因此,局部变量的地址是变化的,而全局变量是不变的。全局变量在编译器就被决定,它存在于可执行模块(.EXE或.DLL等)的文件映像内部。因此,在载入文件的同时,全局变量的内存就被分配了。而局部变量是在例程被调用的时候才分配的。局部变量总是在栈上分配。

  • 动态分配的内存将在堆上进行分配。堆分配与栈分配不同。

  • 简单类型的变量可以用SizeOf()计算内存占用。
    字符串、动态数组、引用参数、无类型参数、过程、接口、对象等类型的变量实际是指针,这些变量由两个部分组成,指针本身和数据体。
    指针本身的内存大小可以用SizeOf()计算,一般是固定大小,32位程序是4字节,64位程序是8字节。
    “数据体”的大小取决于该变量的数据结构,通常有专用的或独立设计的函数来取其大小,如用Length()来取字符串或者数组的大小。

  • 所有的变量名在汇编中都是一个整数,表示内存地址。
    简单变量,相当于汇编中的直接寻址,i表示变量的值,@i表示变量的地址。 指针变量,相当于汇编中的间接寻址,p或者p^表示数据体的值,@p表示变量的地址。 指针变量中保存的是数据体的首地址,可通过Integer(p)将地址强制类型转换为整数。

var
  s: string = '1234567890';
  i: Integer = 15;
  p: PInteger;
  a: array of Integer;

begin
  Writeln('s的值        : ', s);
  Writeln('s的地址      : ', IntToHex(Integer(@s), 8));
  Writeln('s指向的首地址: ', IntToHex(Integer(s), 8));
  Writeln;

  Writeln('i的值        : ', i);
  Writeln('i的地址      : ', IntToHex(Integer(@i), 8));
  Writeln;

  p := @i;
  Writeln('p的值        : ', p^);
  Writeln('p的地址      : ', IntToHex(Integer(@p), 8));
  Writeln('p指向的首地址: ', IntToHex(Integer(p), 8));
  Writeln;

  SetLength(a, 1);
  a[0] := 9;
  Writeln('a[0]的值     : ', a[0]);
  Writeln('a的地址      : ', IntToHex(Integer(@a), 8));
  Writeln('a指向的首地址: ', IntToHex(Integer(a), 8));
  Writeln('a指向的首地址: ', IntToHex(Integer(@a[0]), 8));
  Writeln;

  Readln;

end.
  • var, const,out参数都是引用参数,引用参数在实际传参时并不会将真正的值传递给例程,而是传递参数地址,参数实际是指针。
function GetAddr(const aVar): string;
begin
  Result := IntToHex(Integer(@aVar), 8);
end;

function GetDataAddr(const aVar): string;
begin
  Result := IntToHex(Integer(aVar), 8);
end;
  • 真常量(非类型化的常量)作为立即数处理:将值直接编码到汇编指令中。因此它们不可能有内存占用,当然也不可能有具体的内存地址值。
    除了字符串和集合,Delphi中定义的常量都不能超过8字节(Int64)。否则必须声明成类型化的常量, 类型化的常量与全局变量在同一内存空间中被分配,且使用相同的分配规则。

  • 字符串常量与变量使用的是不同的内存空间。编译器总是将字符串常量与代码放在一起,并将它的首地址直接编码到指令中。
    对于字符串常量来说,并不需要类似于字符串变量的额外空间,因为字符串本身不会被修改,所需要的空间是固定的不会变化,也不需要存储长度(固定的)、引用计数(不需要释放)等。

  • 对于真常量和非类型化的字符串常量来说,取地址是没有意义的,它们实际上没有地址。

  • 字符串使用引用计数和写复制。

  • 集合类型与简单类型一样,没有动态分配的内存。集合中的每个元素占一个位,因为Delphi最大支持256个元素的集合,所以使用1~32个字节来存放各个位数据。集合类型的局部变量总在栈上分配。

  • 不论数组元素是什么类型,静态数组的局部变量总会在栈上分配。如果栈大小不够,将会导致异常。Delphi不会在编译器计算栈的耗用。

  • 动态数组变量只占用4个字节。动态数组在堆中动态分配内存。

  • 缺省情况下,记录会以对齐方式存储。记录总是直接分配内存。变体记录使用能够容纳可变部分最大长度的空间来存储。Delphi约定变体部分必须出现在记录定义的最末部分,这使得在计算和分配空间时,可以通过直接在记录后面附加空间的方式来实现。

  • 文件类型是基于记录类型来实现的,类型文件和无类型文件都是TFileRec,文本文件类型为TTextRec。

  • 指针总是占用4字节的内存(32位程序)。任何时候都可以将指针作为整型使用,并使用整型运算做地址运算。

  • 过程变量只占用4个字节(32位程序)的内存,它实际上是以指针形式存在的。
    过程变量只存储例程代码地址的入口地址,真实的例程代码只存在于应用程序的代码段中。尽管如此,Delphi采用一种与指针不同的语意来理解过程——因此,不能在指针与过程变量之间做强制转换。
    应用程序的代码通常是在编译器由编译程序生成的,一旦编译过程结束,例程代码也就确定了。代码段是只读的。

  • 动态数组的引用计数并非基于元素的,而是基于变量的,这与字符串的引用计数不一样:在字符串中,访问串中的一个字符,会导致引用计数发生变化;而在动态数组中,读写一个元素,并不会使引用关系发生变化。 字符串有写时复制的机制,动态数组没有。

  • 值参数的备份源自于例程的调用约定:值参数可以被改写,但是不会影响到传值的原始变量。因此,编译器需要为值参数制作一个备份,而无论代码中是否要写该值参数。这个复制操作在编译时就被决定了,例程被调用一次,就发生一次复制操作。

  • 使用const声明的值参数,编译器将向例程直接传入参数的地址,如果使用指针访问该地址,就可以对原值进行直接修改。

第3章 BASM精要


最后修改于 2022-01-18