UEFI编写的 Pong 游戏

来自 https://github.com/Openwide-Ingenierie/Pong-UEFI 的 Pong Game 。进行了一个简单的修改,加入 ESC 退出的功能。

pong

原始代码
Pong-UEFI-master

修改后的代码:
uefipong

编译方法很简单, 和之前提到的Shell下面的 Application一样,将工程放在 AppPkg/Application 下面,然后修改 AppPkg.dsc 文件.最后用

build -a IA32 -p AppPkg\AppPkg.dsc

即可生成 EFI, 可以在 NT32 模拟器环境下进行测试.

uefipong

一个失败的 Arduino 项目

古语云“拳是两扇门,全凭脚打人”,无论是中华武术,跆拳道还是空手道,踢腿都是重要的内容。我的教练踢腿速度很快,通常还没有看清脚就已经提到眼前。因此我打算测试踢腿速度的设备,将这个速度进行量化。

方案是使用触摸来判断,在地面和脚靶上分别放置两个导电的装置然后计算从抬脚到接触到的时间差。进行判断接触的传感器还是 MPR121.。最终东西做出来了,但是意外发现这个方案存在致命缺点最后只能放弃。

image001

image002

image003

完整的代码:

#include <Wire.h>
#include "Adafruit_MPR121.h"

// You can have up to 4 on one i2c bus but one is enough for testing!
Adafruit_MPR121 cap = Adafruit_MPR121();

// Keeps track of the last pins touched
// so we know when buttons are 'released'
uint16_t lasttouched = 0;
uint16_t currtouched = 0;

void showdata(unsigned int value)
{
   //这是数码管要求的数据头信息
    Serial.write(0xff);
    Serial.write(0x00);
    Serial.write(0x04);  //显示四位数值
    if (value>9999) {  // 9999
        Serial.write(0x09);
        Serial.write(0x09);
        Serial.write(0x09);
        Serial.write(0x09);
    } else
        if (value >999) { //9.999
           Serial.write(value / 1000 + 0x80);
           Serial.write((value - value /1000 * 1000) / 100);      
           Serial.write((value - value /100 * 100) / 10); 
           Serial.write(value % 10);
        }
        else
           {
           Serial.write(value / 1000);
           Serial.write((value - value /1000 * 1000) / 100);      
           Serial.write((value - value /100 * 100) / 10); 
           Serial.write(value % 10);
          }
              
      
   //最后一位是亮度
     Serial.write(0);
      
}

// the setup function runs once when you press reset or power the board
void setup() {
  Serial.begin(115200);
 

  // Default address is 0x5A, if tied to 3.3V its 0x5B
  // If tied to SDA its 0x5C and if SCL then 0x5D
  if (!cap.begin(0x5A)) {
    Serial.println("MPR121 not found, check wiring?");
    while (1);
  }
 // Serial.println("MPR121 found!");  
}

int elsp=0;

// the loop function runs over and over again forever
void loop() {
   // Get the currently touched pads
  currtouched = cap.touched();

  //Touch 11 Release -- Start
  //Touch 7  Touch -- End
  //Tpuch 5  Reset

  if (!(currtouched & _BV(11)) && (lasttouched & _BV(11)) ) {
      if (elsp==0) {
       // Serial.println("Start");
        elsp=millis();
      }
    }
  if ((currtouched & _BV(7)) && !(lasttouched & _BV(7)) ) {
      if (elsp!=0) {
           // Serial.println("Stop");
            //Serial.println(millis()-elsp);
            showdata(millis()-elsp);
            elsp=0;
          }
        }

  if (!(currtouched & _BV(5)) && (lasttouched & _BV(5)) ) {
      //Serial.println("reset timer");
      showdata(0);
      elsp=0;
    }

  // reset our state
  lasttouched = currtouched;
  if (elsp!=0) {showdata(millis()-elsp);}

}

 

工作的视频

遇到的问题如下:
1. MPR121 是通过母座插在一块 Shield板上的,但是座子和MPR121接触不好,这对于触摸传感器来说是致命的。最后只得直接从压线座飞线到 MPR121上;
2. 负责接触脚的金属一直有问题。起初使用的是和之前石头剪刀布游戏机一样的纸壳贴锡箔,但是测试发现一脚上去就会碎掉。后来我特地用502沾了一个结果发现感应非常不灵敏,可能是因为面积过大导致的。之前的石头剪刀布游戏机最终人是要站在上面的,有充分的时间来进行感知,而这次的装置,脚接触一次很快就会收回,因此对于准确性和灵敏度要求都会比较高;
3. 因为是触摸感应,所以对线的长度接触之类是比较敏感的,在使用中发现布线会比较麻烦,脚靶那一端经常会被“踢飞”,有了线之后会影响发挥;
综合上述的原因,使用触摸的方案来进行踢腿速度测试是不可行的。

当然我们还是能从这个设备学习到很多,首先是前面的三点失败的总结,此外还有对于那种大的数码管控制,在未来通过其他方法改进,我相信总有一天能够做出测试踢腿速度的设备.

Step to UEFI (118)新指令 RDRand

从 IvyBridge开始, Intel 新加入了 RDRAND 和 RDSEED 两个用于生成随机数的指令。从【参考1】来看二者的差别在于:

The short answer
The decision process for which instruction to use is mercifully simple, and based on what the output will be used for.
• If you wish to seed another pseudorandom number generator (PRNG), use RDSEED
• For all other purposes, use RDRAND
That’s it. RDSEED is intended for seeding a software PRNG of arbitrary width. RDRAND is intended for applications that merely require high-quality random numbers.

简单的说二者的差别就是 “如果打算用来作为其它伪随机数生成器的种子的时候那么就可以考虑RDSEED,不然就使用RNRAND。”【参考2】

在 UDK2015的代码中,有涉及到 RDRAND这个指令的,下面就进行实验。作为参考的代码在 SecurityPkg\RandomNumberGenerator\RngDxe 下面。比较特别的地方是,代码中用到了汇编语言调用这个指令,例如:

;------------------------------------------------------------------------------
;  Generate a 16 bit random number
;  Return TRUE if Rand generated successfully, or FALSE if not
;
;  BOOLEAN EFIAPI RdRand16Step (UINT16 *Rand);   RCX
;------------------------------------------------------------------------------
RdRand16Step  PROC
    ; rdrand   ax                  ; generate a 16 bit RN into ax, CF=1 if RN generated ok, otherwise CF=0
    db     0fh, 0c7h, 0f0h         ; rdrand r16:  "0f c7 /6  ModRM:r/m(w)"
    jb     rn16_ok                 ; jmp if CF=1
    xor    rax, rax                ; reg=0 if CF=0
    ret                            ; return with failure status
rn16_ok:
    mov    [rcx], ax
    mov    rax, 1
    ret
RdRand16Step ENDP

 

而对于 X64 来说,无法实现代码中内嵌汇编,因此,源程序上有一份32的ASM 和一份 64 的 ASM。

INF 文件中也要分开声明两次:

[Sources.common]
  RdRand.c
  RdRand.h
[Sources.IA32]
  IA32/RdRandWord.c
  IA32/AsmRdRand.asm
[Sources.X64]
  X64/RdRandWord.c
  X64/AsmRdRand.asm

 

此外,在使用这个指令之前还需要用CPUID指令来检测当前CPU是否支持,最终代码如下:

/** @file
  Support routines for RDRAND instruction access.

Copyright (c) 2013, Intel Corporation. All rights reserved.<BR>
This program and the accompanying materials
are licensed and made available under the terms and conditions of the BSD License
which accompanies this distribution.  The full text of the license may be found at
http://opensource.org/licenses/bsd-license.php

THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.

**/
#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/UefiLib.h>

#include "RdRand.h"
//#include "AesCore.h"

//
// Bit mask used to determine if RdRand instruction is supported.
//
#define RDRAND_MASK    0x40000000

/**
  Determines whether or not RDRAND instruction is supported by the host hardware.

  @retval EFI_SUCCESS          RDRAND instruction supported.
  @retval EFI_UNSUPPORTED      RDRAND instruction not supported.

**/
EFI_STATUS
EFIAPI
IsRdRandSupported (
  VOID
  )
{
  EFI_STATUS  Status;
  UINT32      RegEax;
  UINT32      RegEbx;
  UINT32      RegEcx;
  UINT32      RegEdx;
  BOOLEAN     IsIntelCpu;

  Status     = EFI_UNSUPPORTED;
  IsIntelCpu = FALSE;
  
  //
  // Checks whether the current processor is an Intel product by CPUID.
  //
  AsmCpuid (0, &RegEax, &RegEbx, &RegEcx, &RegEdx);
  if ((CompareMem ((CHAR8 *)(&RegEbx), "Genu", 4) == 0) &&
      (CompareMem ((CHAR8 *)(&RegEdx), "ineI", 4) == 0) &&
      (CompareMem ((CHAR8 *)(&RegEcx), "ntel", 4) == 0)) {
    IsIntelCpu = TRUE;
  }

  if (IsIntelCpu) {
    //
    // Determine RDRAND support by examining bit 30 of the ECX register returned by CPUID.
    // A value of 1 indicates that processor supports RDRAND instruction.
    //
    AsmCpuid (1, 0, 0, &RegEcx, 0);

    if ((RegEcx & RDRAND_MASK) == RDRAND_MASK) {
      Status = EFI_SUCCESS;
    }
  }

  return Status;
}

/**
  Calls RDRAND to obtain a 16-bit random number.

  @param[out]  Rand          Buffer pointer to store the random result.
  @param[in]   NeedRetry     Determine whether or not to loop retry.

  @retval EFI_SUCCESS        RDRAND call was successful.
  @retval EFI_NOT_READY      Failed attempts to call RDRAND.

**/
EFI_STATUS
EFIAPI
RdRand16 (
  OUT UINT16       *Rand,
  IN BOOLEAN       NeedRetry
  )
{
  UINT32      Index;
  UINT32      RetryCount;

  if (NeedRetry) {
    RetryCount = RETRY_LIMIT;
  } else {
    RetryCount = 1;
  }

  //
  // Perform a single call to RDRAND, or enter a loop call until RDRAND succeeds.
  //
  for (Index = 0; Index < RetryCount; Index++) {
    if (RdRand16Step (Rand)) {
      return EFI_SUCCESS;
    }
  }
  
  return EFI_NOT_READY;
}

/**
  Calls RDRAND to obtain a 32-bit random number.

  @param[out]  Rand          Buffer pointer to store the random result.
  @param[in]   NeedRetry     Determine whether or not to loop retry.

  @retval EFI_SUCCESS        RDRAND call was successful.
  @retval EFI_NOT_READY      Failed attempts to call RDRAND.

**/
EFI_STATUS
EFIAPI
RdRand32 (
  OUT UINT32       *Rand,
  IN BOOLEAN       NeedRetry
  )
{
  UINT32      Index;
  UINT32      RetryCount;

  if (NeedRetry) {
    RetryCount = RETRY_LIMIT;
  } else {
    RetryCount = 1;
  }

  //
  // Perform a single call to RDRAND, or enter a loop call until RDRAND succeeds.
  //
  for (Index = 0; Index < RetryCount; Index++) {
    if (RdRand32Step (Rand)) {
      return EFI_SUCCESS;
    }
  }
  
  return EFI_NOT_READY;
}

/**
  The user Entry Point for Application. The user code starts with this function
  as the real entry point for the application.

  @param[in] ImageHandle    The firmware allocated handle for the EFI image.
  @param[in] SystemTable    A pointer to the EFI System Table.

  @retval EFI_SUCCESS       The entry point is executed successfully.
  @retval other             Some error occurs when executing this entry point.

**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
	UINT16	RandNumber1;
	UINT32	RandNumber2;
	
	if	(FALSE==IsRdRandSupported) {
			Print (L"Your CPU doesn't support RdRand\n");
			return EFI_SUCCESS;
	}
	RdRand16(&RandNumber1,TRUE);
	Print (L"Generate a 16 bits number [%X]\n",RandNumber1);
	
	RdRand32(&RandNumber2,TRUE);
	Print (L"Generate a 32 bits number [%X]\n",RandNumber2);
	
    return EFI_SUCCESS;
}

 

特别注意:代码无法在 NT32环境下运行,下面的结果是在 KBL-R HDK 上取得的。

rnrand

IA32和X64的 Application 下载:
RdRandapp

完整的代码下载
RdRand

参考:
1. https://software.intel.com/en-us/blogs/2012/11/17/the-difference-between-rdrand-and-rdseed The Difference Between RDRAND and RDSEED
2. http://blog.yinfupai.com/2914.html

Step to UEFI (116)CTRL+ALT+DEL输出字符

最近看代码,发现比较有意思的地方,在 Keyboard.c 中有处理 USB键盘 Ctrl+Alt+Del的代码。

    //
    // When encountering Ctrl + Alt + Del, then warm reset.
    //
    if (KeyDescriptor->Modifier == EFI_DELETE_MODIFIER) {
      if ((UsbKeyboardDevice->CtrlOn) && (UsbKeyboardDevice->AltOn)) {
        gRT->ResetSystem (EfiResetWarm, EFI_SUCCESS, 0, NULL);
      }
}

 

修改一下即可实现在 Shell下按下 Ctrl+Alt+Del输出一段字符,暂停5s然后重启的功能。

    //
    // When encountering Ctrl + Alt + Del, then warm reset.
    //
    if (KeyDescriptor->Modifier == EFI_DELETE_MODIFIER) {
      if ((UsbKeyboardDevice->CtrlOn) && (UsbKeyboardDevice->AltOn)) {
		//LABZDebug_Start
		gST->ConOut->OutputString(gST->ConOut,L"www.lab-z.com");
		gBS->Stall(5000000UL);
		// LABZDebug _End
        gRT->ResetSystem (EfiResetWarm, EFI_SUCCESS, 0, NULL);
      }
    }

 

再阅读代码,上面的代码在USBParseKey 函数中。这个函数实现的是解析 USB键盘的功能。函数调用者是EfiKey.c 中的

/**
  Timer handler to convert the key from USB.

  @param  Event                    Indicates the event that invoke this function.
  @param  Context                  Indicates the calling context.
**/
VOID
EFIAPI
USBKeyboardTimerHandler (
  IN  EFI_EVENT                 Event,
  IN  VOID                      *Context
  )

 

从名称上看,这是一个时间中断来调用的函数。具体的安装代码如下:

  Status = gBS->CreateEvent (
                  EVT_TIMER | EVT_NOTIFY_SIGNAL,
                  TPL_NOTIFY,
                  USBKeyboardTimerHandler,
                  UsbKeyboardDevice,
                  &UsbKeyboardDevice->TimerEvent
                  );

 

就是说USB键盘是通过时间中断来进行检查和处理的.

UEFI编写的迷宫游戏

最近在 GitHub 上看到了一个 UEFI 编写的迷宫游戏(作者 Haodong Liu , https://github.com/liute62/Firmware-UEFI-Maze-Game), 感觉界面不是很漂亮,于是拿过来进行简单的修改:

maze

修改之前的原始代码和资料:
Firmware-UEFI-Maze-Game-master

修改之后的代码
uefimaze

用的按键是上下左右,ECS 还有 PageUp(类似 Enter的功能)。

VS2015 下面编译 SPB 驱动

差不多3年前的这个时候,我写过一篇介绍如何在 VS2013下面编译 Windows Driver Sample的文章【参考1】。今年的这个时候,我再一次尝试在 VS2015下面编译驱动。

For building Windows Sample you need below software

1. Windows 10 RS2: 15063.0.170317-1834.RS2_RELEASE_CLIENTPRO-CORE_OEMRET_X64FRE_EN-US (Note: VS2015 can’t be installed on Windows7)
2. 15063.0.170317-1834.rs2_release_amd64fre_WDK.iso
3. 15063.0.170317-1834.rs2_release_WindowsSDK.iso
4. Visual_Studio_Pro_2015_English.iso

1.Install Windows RS2
2.Install VS2015 (2 Hours),全默认配置

3.Install WDK 到默认的路径下

image001

安装完成后,用 VS2015打开驱动工程文件。编译会出现下面的错误

“—— Build started: Project: SpbTestTool (Exe\SpbTestTool), Configuration: Debug Win32 ——
Building ‘SpbTestTool’ with toolset ‘WindowsApplicationForDrivers10.0’ and the ‘Desktop’ target platform.
TRACKER : error TRK0005: Failed to locate: “CL.exe”. The system cannot find the file specified.

—— Build started: Project: SpbTestTool (Sys\SpbTestTool), Configuration: Debug Win32 ——
Building ‘SpbTestTool’ with toolset ‘WindowsKernelModeDriver10.0’ and the ‘Universal’ target platform.
Stamping .\Debug\\SpbTestTool.inf [Version] section with DriverVer=05/18/2017,18.15.36.668
TRACKER : error TRK0005: Failed to locate: “CL.exe”. The system cannot find the file specified.

========== Build: 0 succeeded, 2 failed, 0 up-to-date, 0 skipped ==========”

根据我的研究,这是因为VS2015默认安装没有带C++编译器(有点莫名其妙)

image002

image003

image004

再进行一次编译:
—— Build started: Project: SpbTestTool (Exe\SpbTestTool), Configuration: Debug x64 ——
Building ‘SpbTestTool’ with toolset ‘WindowsKernelModeDriver10.0’ and the ‘Desktop’ target platform.
command.cpp
command.cpp : fatal error C1083: Cannot open include file: ‘C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\shared\warning.h’: No such file or directory
main.cpp
main.cpp : fatal error C1083: Cannot open include file: ‘C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\shared\warning.h’: No such file or directory
util.cpp
util.cpp : fatal error C1083: Cannot open include file: ‘C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\shared\warning.h’: No such file or directory
Generating Code…
—— Build started: Project: SpbTestTool (Sys\SpbTestTool), Configuration: Debug x64 ——
Building ‘SpbTestTool’ with toolset ‘WindowsKernelModeDriver10.0’ and the ‘Universal’ target platform.
Stamping .\x64\Debug\\SpbTestTool.inf [Version] section with DriverVer=05/18/2017,22.28.17.712
driver.cpp
driver.cpp : fatal error C1083: Cannot open include file: ‘C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\shared\warning.h’: No such file or directory
device.cpp
device.cpp : fatal error C1083: Cannot open include file: ‘C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\shared\warning.h’: No such file or directory
peripheral.cpp
peripheral.cpp : fatal error C1083: Cannot open include file: ‘C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\shared\warning.h’: No such file or directory
Generating Code…
========== Build: 0 succeeded, 2 failed, 0 up-to-date, 0 skipped ==========

这个错误是因为没有安装 SDK 导致的(VS2015自带SDK,但是可能版本和 WDK 的不匹配,所以有问题)。再安装 SDK,之后 Build again
—— Build started: Project: SpbTestTool (Exe\SpbTestTool), Configuration: Debug x64 ——
Building ‘SpbTestTool’ with toolset ‘WindowsKernelModeDriver10.0’ and the ‘Desktop’ target platform.
command.cpp
main.cpp
util.cpp
Generating Code…
SpbTestTool.vcxproj -> C:\spb\SpbTestTool\exe\x64\Debug\SpbTestTool.exe
SpbTestTool.vcxproj -> x64\Debug\SpbTestTool.pdb (Full PDB)
Inf2Cat task was skipped as there were no inf files to process

—— Build started: Project: SpbTestTool (Sys\SpbTestTool), Configuration: Debug x64 ——
Building ‘SpbTestTool’ with toolset ‘WindowsKernelModeDriver10.0’ and the ‘Universal’ target platform.
Stamping .\x64\Debug\\SpbTestTool.inf [Version] section with DriverVer=05/19/2017,0.46.12.487
driver.cpp
device.cpp
peripheral.cpp
Generating Code…
SpbTestTool.vcxproj -> C:\spb\SpbTestTool\sys\x64\Debug\SpbTestTool.sys
SpbTestTool.vcxproj -> x64\Debug\SpbTestTool.pdb (Full PDB)
Done Adding Additional Store
Successfully signed: C:\spb\SpbTestTool\sys\x64\Debug\SpbTestTool.sys

Driver is a Universal Driver.
……………………
Signability test complete.

Errors:
None

Warnings:
None

Catalog generation complete.
C:\spb\SpbTestTool\sys\x64\Debug\SpbTestTool\spbsamples.cat
Done Adding Additional Store
Successfully signed: x64\Debug\SpbTestTool\spbsamples.cat

========== Build: 2 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

成功!

本文提到的驱动是 Windower Driver Sample【参考2】的一部分。

这里我放置一个 SPB 的Driver

SpbTestTool

参考:
1.http://www.lab-z.com/spbtesttool/
2.https://github.com/Microsoft/windows-driver-samples

Shell 下的 I2C 工具

最近写了一个 Shell下的 I2C 访问工具,可以帮助大家进行 Shell下的I2C测试.

image001

主要功能有4个:
1. Read 读取给定的 I2C Bus, Slave Address, Register 的值
2. Write 向指定的 I2C Bus, Slave Address, Register 写入Value
3. Dump 枚举指定的 I2C Bus, Slave Address上面的256 个Register
4. Scan 扫描枚举指定的 I2C Bus,列出所有对于读操作有 act的设备地址(128个)

A. Read 的功能
image002

B. Dump 功能
image003

C. Scan Bus 确定 I2C 设备地址的功能 ( 这是在 KBL-R HDK 板子上接入了一个 I2C Touch 后的扫描结果,可以看到一个设备上有很多个地址)

image004

下载(无 Source Code)

2017年6月17日更新,之前的版本是 0.3 ,当前升级到了 0.4 修改了 write 无法正确写入的问题

zI2c

How to debug ACPI by WinDBG in Winows RS2

自从 Windows 升级到了 RS1 , WinDBG 就无法进行ACPI 的调试了,主要的现象是各种找不到变量函数等等。经过一番研究,终于摸索出来了在 RS2 上进行ACPI 调试的方法。具体步骤如下:
特别注意,第一次实验的时候务必按照顺序进行,否则很可能需要重头再来!
1. Target端安装 Windows RS2, 用户名为ZTX (请使用这个名称,确保后面的操作有参考作用)
image001

2. Target端进入 Windows后,运行 MSConfig

image002image003

3. Target端CMD 串口下(Administrator 权限),运行下面的命令要求系统不检查驱动签名

bcdedit.exe -set TESTSIGNING ON

4. Host 端使用 WinDbg 确保这个步骤能连上,如果连不上请检查USB设定
5. Target端进入 Windows/System32 下搜索 “acpi.sys”,会找到很多结果,我们的目标只是 acpi.sys,需要将系统中的这个文件替换为 checked 版本的。通常系统中会有1个以上的ACPI.SYS,一个是原本的驱动,另外是系统缓存出来的,所有的都需要删除
image004

因为系统保护的缘故,删除动作很满烦,具体操作如下:
5.1 选择第一个 ACPI.SYS,打开 Properties –> Security查看 Users, 注意到是没有Modify 的权限的。
image005

5.2 选择 Advanced, 点击 Owner:TrustedInstaller 后面的Change
image006

输入用户名 ZTX

image007

之后 Owner 会变成 ZTX
image008

返回上一层之后选择 Users
image010

然后使用 edit 按钮,选中 Users 然后再选择 Full Control
image011

出现下面的提示,选择 Yes
image012

之后,你就有权限删除所有的 ACPI.SYS了。然后将 checked 版本的 ACPI.SYS 放置到system32\drivers目录下。
至此就完成了替换,重启 Target端,
重启之后,HOST端的 WinDBG 会出现下面的提示(特别提醒,这里一定要连着WinDBG,否则看到的只能是无尽的BSOD):
image013

输入 g 还会再次停下来
image014
再输入 gh ,即可进入系统。
image015

简单的托盘程序2

基于很早之前(那时候还有 MSN )编写的托盘程序,可以在内存中查找指定的进程(本例中查找的是Notepad.exe),如果找到了就在 memo 中记录当前时间。

编译环境为 Delphi XE2。

totray2

2017/5/30

参考:
1。http://www.lab-z.com/%E3%80%902009%E5%B9%B41%E6%9C%8816%E6%97%A5%E3%80%91delphi-%E5%BD%93%E6%9C%80%E5%B0%8F%E5%8C%96%E6%97%B6%EF%BC%8C%E8%87%AA%E5%8A%A8%E7%BC%A9%E5%87%8F%E5%88%B0%E6%89%98%E7%9B%98%E4%B8%AD/