Bennie 发表于 2005-8-15 00:00:00

如何用Keil ARM编译器作x65的Patch

主要以"替换运营商标志为自定义字串V2"为例说一下怎么用Keil ARM编译器来做Patch或者移植。

最近一段时间,因为Patch的原因,我试用了所有的主流ARM编译器,包括ADS1.2,RealView2.0,GCC for ARM,Keil ARM。最后发现,最合适的工具就是Keil ARM。虽然还有一些小问题,不过起来最解决。首先,Keil在代码定位方面是最容易的,ADS和GCC都需要用独立的链接文件才能做到Patch这样的松散定位,但仍然很费劲,而且比如ADS需要你的段开始于4字节的边界。这是很麻烦的,很多时候我们要Hook的BL指令开始于2字节边界,此外,这些编译器对于调用地址有很多限制,比如简单的BL指令,在ADS中就很难完成,而BLX指令在ADS中甚至会出错。
痛苦中,我甚至打算自己写一个汇编器,实际我也完成了词法和语法,可以处理简单的BL和BLX,不过从avkiev那里得到解决BLX的办法后,我还是放弃了这个计划。虽然Keil有一些缺陷,但毕竟可以忍受。为了配合Keil,我又重新写了一下Hex2VKP工具,目的只有一个,使Patch工作变简单,目标就是像Riza的sfe那样。(我的编译器计划是apm,ARM Patch Maker)。
下面我就结合"替换运营商标志为自定义字串V2"说明一下基本步骤,为了这个,我已经把它完全移植到Keil下了。下面是这个Patch的所有工程文件。
这个工程里面主要有三个源文件,startup.s、RepSP.c、x65.h。还有Keil的工程文件RepSP.Uv2和Hex2vkp这个工具。startup.s是汇编文件,主要是Hook部分及一些Thunk的函数。RepSP.c是主文件,里面包括了所有的程序部分。而x65.h是头文件,里面定义了一些函数原形和结构体。hex2vkp是我专门为hex文件转换为vkp而写的工具,最新重写了一下,增强一下功能,它的格式如下:
hex2vkp [-h? -bbaseaddr -fflashfile -ccommentsfile -ovkpfile]
      -cspecify a commentsfile to describe vkp, all line should be output
            until a line begin with ';;' is found.
      -bspecify base address of fullflash, default A0000000
      -fspecify original fullflash file
      -hproduce this help message
      -ospecify output vkp file, default to console
      -vreport hex2vkp version info
可以指定基地址,默认是A0000000,fullflash文件,注释文件和输出文件。同时输入可以指定一系列的hex文件。
我们主要看一下这个工程中和地址定位相关的部分。

首先是整个工程的地址,在这里面我使用了0x800000开始的Flash部分,指定是在工程的目标选项的的LA Location的里面,作如下指定。
User Classes:DATA (0xA8000000-0xA87FFFFF), CODE (0xA0800040-0xA081FFFF), CONST (0xA0800040-0xA081FFFF)
这里面主要指定了程序的数据部分、代码部分、常量区。
然后是Hook的地址指定,这个我们看一下startup.s。
;***替换运营商标志为自定义字串V2.1***
;(C) Bennie
;for S6CV50
;v2.1->重写了代码

;支持的格式:
;%Y-   年(四位数字)2005
;%y-   年(两位数字)05
;%M-   月06
;%D-   日23
;%W-   星期    四
;%H-   时 00
;%F-   分 56
;%R-   信号强度 -46
;%A-   电池电压 3807
;%a-   电池电压,百分数 56
;%T-   环境温度 30,74
;对齐格式字符:0xE01C=中,0xE01D=右,0xE01E=左

;屏幕保护自定义字串,注意必须以\0结尾,最大15个字符。
;0x800000: '%M-%D %W %H:%F\0'
;待机画面自定义字串
;0x800020: '%R %A %T\0'
;;==============================================================

           EXTERN CODE16(entryMainscreen?T)
           EXTERN CODE16(entryScreensaver?T)

;待机时调用获得运营商名字的函数Hook
AREA   STARTUPCODE1, CODE, AT 0xA0AFCB82   // READONLY, ALIGN=4
        CODE16
           PUSH        {LR}
           BL         entryScreensaver?T
           POP        {PC}
                                                                       
;屏保时调用获得运营商名字的函数Hook
AREA   STARTUPCODE2, CODE, AT 0xA08DAF2E   // READONLY, ALIGN=4
        CODE16

           BL         entryMainscreen?T

;函数Thunk,处理一下dwmoddw函数   
AREA   THUNKFUNCTION, CODE, AT 0xA0800040   // READONLY, ALIGN=4

        PUBLIC         dwmoddw?T

        dwmoddw?T PROC CODE16
           LDR        R2, =0xA160948C
           BX         R2
        ENDP
END

前面的部分是最终vkp的注释部分,这个由hex2vkp输出到vkp的开始。然后前面两个Extern知名了这两个函数是外部函数,然后第一个AREA开始的部分指定了一个区段,后面是这个区段的属性,比如属于那个段(代码段)、对齐边界(默认是4),以及开始地址。可以指定地址是我选择Keil的主要原因,在别的工具上,做这个就困难多了。然后下面的CODE16是说明下面是THUMB指令,在下面就是代码部分了。下面的部分类似,除了最后一个是函数THUNK有点不同,那个主要原因是KEIL在处理多个函数返回值时,用函数指针有点问题,所以我用了C和ASM的混合链接。前面那个PUBLIC是导出一个符号,这里面体现了KEIL对混合编译的符号处理。导出的符号要加上?T或?A。才可以被别的模块使用。后面声名了一个Thumb例程。

下面我们再看一下RepSP.c里面和地址定位有关的部分。
#include "x65.h"

#define S6CV50
/*
0x800000: '%M-%D %W %H:%F\0'
0x800020: '%R %A %T\0'
*/
const word format __at 0xA0800000;

const unsigned short w1_7[] = {          
        0x4E00, //一
        0x4E8C,        //二
        0x4E09,        //三
        0x56DB,        //四
        0x4E94,        //五
        0x516D,        //六       
        0x65E5        //日
};

#ifdef S6CV50
//for system function
const GETDATETIME getdate         = (GETDATETIME)0xA0AA6A53;
const GETWEEK getweek                 = (GETWEEK)0xA082AD87;
const GetAkku getakku                 = (GetAkku)0xA1254830;
const f_uint2str uint2str         = (f_uint2str)0xA0825687;
//const DWMODDW dwmoddw         = (DWMODDW)0xA160948C; //because Keil's problem

//for system data with Absolute Location.
word Akkpct __at 0xA863FF08;
word net         __at 0xA86F0BB8;
#endif

extern int_int dwmoddw(int a, int b);
。。。。。。。
无关的部分我已经省略,前面是包含头文件,之后定一个宏,在下面那个格式字串就涉及到了变量的决定定址,在Keil里面试用__at关键词。然后后面是一个常量数组,需要注意的是这些一定要声明为常量,否则会被定位到数据区去。在之后就是本工程使用的函数部分。这个函数原形的声明在x65.h里面,再后面是两个全局变量的绝对定址。分别时电池容量百分比和网络信号强度。这些是和地址有关的部分,在下面的一个声明是为了和startup.s里面联合编译使用的声明部分,Keil对这种用R0-R3返回变量的功能支持也有问题,所以我用了一个Thunk。

此外,我们要设置工程的输出文件为Intel的Hex格式,然后还要设置附加的命令,让他编译完成后自动转换Hex为VKP,那么在Outpu选项卡里面不仅要设定输出格式,还要在User Program那里写下如下的命令:
.\hex2vkp -fE:\siemens\tools\S6CV50.bin-cstartup.s -oRepSP.vkp output\RepSP.hex
意思是原始文件是E:\siemens\tools\S6CV50.bin,这个需要改成你自己的,注释来自于startup.s,输出为RepSP.vkp。输入是output\RepSP.hex。
这样在编译完成后就会自动生成vkp了。

那么,了解了这个工程的各部分以后,我们看一下为了移植这个工程,你需要处理那些部分。可能这才是大家需要的!
移植这个需要处理的函数有:
const GETDATETIME getdate         = (GETDATETIME)0xA0AA6A53;
const GETWEEK getweek                 = (GETWEEK)0xA082AD87;
const GetAkku getakku                 = (GetAkku)0xA1254830;
const f_uint2str uint2str         = (f_uint2str)0xA0825687;
//const DWMODDW dwmoddw         = (DWMODDW)0xA160948C; //because Keil's problem

AREA   THUNKFUNCTION, CODE, AT 0xA0800040   // READONLY, ALIGN=4

        PUBLIC         dwmoddw?T

        dwmoddw?T PROC CODE16
           LDR        R2, =0xA160948C
           BX         R2
        ENDP
需要注意的是,如果是Thumb模式的函数,因为是函数指针,最后一个bit为1。即在函数地址上加1。特别的是dwmoddw是在startup.s里面用thumb处理的。涉及到的全局变量有:
word Akkpct __at 0xA863FF08;
word net         __at 0xA86F0BB8;涉及到的函数Hook有:
AREA   STARTUPCODE1, CODE, AT 0xA0AFCB82   // READONLY, ALIGN=4
AREA   STARTUPCODE2, CODE, AT 0xA08DAF2E   // READONLY, ALIGN=4
还有就是Patch的占用地址了,这个在工程选项里设定。

对于这些,我们从几个方面入手:
从待机的显示和处理可以找到getdategetweek dwmoddw 以及信号强度的变量地址
从工模的ACCU菜单可以找到getakkuuint2str 和电池的百分比容量。
那么我分别给出S65的类似部分。

A1253530             ; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹?
A1253530
A1253530
A1253530             wndACCUMonitor                        ; CODE XREF: j_wndACCUMonitorj
A1253530                                                   ; DATA XREF: ROM:off_A0A2EBD8o
A1253530
A1253530             var_34          = -0x34
A1253530             var_30          = -0x30
A1253530             var_2C          = -0x2C
A1253530
A1253530 F0 4D 2D E9               STMFD   SP!, {R4-R8,R10,R11,LR}
A1253534 01 40 A0 E1               MOV   R4, R1
A1253538 00 70 A0 E1               MOV   R7, R0
A125353C 04 00 A0 E1               MOV   R0, R4
A1253540 4C 13 9F E5               LDR   R1, =asc_A1410094
A1253544 18 D0 4D E2               SUB   SP, SP, #0x18
A1253548 9A 20 A0 E3               MOV   R2, #0x9A
A125354C 71 D7 0E EB               BL      memcpy
A1253550 0C 20 A0 E3               MOV   R2, #0xC
A1253554 CF 1F 8F E2               ADR   R1, asc_A1253898 ; "AkkuMon (1)"
A1253558 07 00 A0 E1               MOV   R0, R7
A125355C 6D D7 0E EB               BL      memcpy
A1253560 09 10 A0 E3               MOV   R1, #9
A1253564 00 00 A0 E3               MOV   R0, #0
A1253568 B0 04 00 EB               BL      GetAkku
A125356C 08 00 8D E5               STR   R0,
A1253570 00 00 A0 E3               MOV   R0, #0
A1253574 07 10 A0 E3               MOV   R1, #7
A1253578 AC 04 00 EB               BL      GetAkku
A125357C 04 00 8D E5               STR   R0,
A1253580 00 00 A0 E3               MOV   R0, #0
A1253584 08 10 A0 E3               MOV   R1, #8
A1253588 A8 04 00 EB               BL      GetAkku
A125358C 00 50 A0 E1               MOV   R5, R0
A1253590 00 00 A0 E3               MOV   R0, #0
A1253594 06 10 A0 E3               MOV   R1, #6
A1253598 A4 04 00 EB               BL      GetAkku
这个地方那个可以找到GetAKKU和uint2str 和百分比容量

A12536F0 7A 00 84 E2               ADD   R0, R4, #0x7A
A12536F4 07 D7 0E EB               BL      memcpy
A12536F8 9C 04 1F E5               LDR   R0, =0xA863FF08   ;这个地址就是百分比容量
A12536FC 04 20 A0 E3               MOV   R2, #4
A1253700 0C 10 8D E2               ADD   R1, SP, #0x38+var_2C
A1253704 B0 00 D0 E1               LDRH    R0,
A1253708 DD 47 D7 FB               BLX   uint2str
A125370C 04 20 A0 E3               MOV   R2, #4
A1253710 0C 10 8D E2               ADD   R1, SP, #0x38+var_2C
A1253714 82 00 84 E2               ADD   R0, R4, #0x82
A1253718 FE D6 0E EB               BL      memcpy
A125371C CC 54 1F E5               LDR   R5, =0xA863FF00
A1253720 18 00 95 E5               LDR   R0,
A1253724 00 00 50 E3               CMP   R0, #0
A1253728 38 00 00 0A               BEQ   loc_A1253810
A125372C B2 00 D0 E1               LDRH    R0,
A1253730 04 20 A0 E3               MOV   R2, #4
A1253734 0C 10 8D E2               ADD   R1, SP, #0x38+var_2C
A1253738 D1 47 D7 FB               BLX   uint2str

从GetSPNameWrap出发,可以找到待机和屏保时的Hook地址,如下:
A08DAF28             ; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹?
A08DAF28
A08DAF28
A08DAF28             GetSPNameWrap                           ; CODE XREF: ROM:A08DB0C4p
A08DAF28                                                   ; ROM:A08DB176p ...
A08DAF28 70 B5                     PUSH    {R4-R6,LR}
A08DAF2A 05 1C                     ADD   R5, R0, #0
A08DAF2C 00 21                     MOV   R1, #0
A08DAF2E 21 F2 28 FE               BL      GetSPName    ;待机时Hook的就是这个地址。屏保时Hook的是GetSPName这个函数的开始。
A08DAF32 01 28                     CMP   R0, #1
A08DAF34 17 D1                     BNE   loc_A08DAF66
A08DAF36 28 1C                     ADD   R0, R5, #0
A08DAF38 52 F1 0A E9               BLX   sub_A0A2D150
A08DAF3C 06 1C                     ADD   R6, R0, #0
A08DAF3E 01 24                     MOV   R4, #1
A08DAF40 0D E0                     B       loc_A08DAF5E
A08DAF42             ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
A08DAF42
A08DAF42             loc_A08DAF42                            ; CODE XREF: GetSPNameWrap+38j
A08DAF42 21 1C                     ADD   R1, R4, #0
A08DAF44 28 1C                     ADD   R0, R5, #0
A08DAF46 52 F1 74 EE               BLX   sub_A0A2DC30
A08DAF4A 0A 28                     CMP   R0, #0xA
A08DAF4C 04 D1                     BNE   loc_A08DAF58
A08DAF4E 22 1C                     ADD   R2, R4, #0
A08DAF50 20 21                     MOV   R1, #0x20
A08DAF52 28 1C                     ADD   R0, R5, #0
A08DAF54 52 F1 3E EE               BLX   sub_A0A2DBD4
A08DAF58
A08DAF58             loc_A08DAF58                            ; CODE XREF: GetSPNameWrap+24j
A08DAF58 01 34                     ADD   R4, #1
A08DAF5A 24 04                     LSL   R4, R4, #0x10
A08DAF5C 24 0C                     LSR   R4, R4, #0x10
A08DAF5E
A08DAF5E             loc_A08DAF5E                            ; CODE XREF: GetSPNameWrap+18j
A08DAF5E B4 42                     CMP   R4, R6
A08DAF60 EF D9                     BLS   loc_A08DAF42
A08DAF62 01 20                     MOV   R0, #1
A08DAF64 70 BD                     POP   {R4-R6,PC}
A08DAF66             ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
A08DAF66
A08DAF66             loc_A08DAF66                            ; CODE XREF: GetSPNameWrap+Cj
A08DAF66 00 20                     MOV   R0, #0
A08DAF68 70 BD                     POP   {R4-R6,PC}

GetDate可以查找类似的特征码,而GetWeek也可以,dwMODdw函数就在GetWeek里面,不过可能是一个Thunk跳转。
A082AD86
A082AD86             GetWeek                                 ; DATA XREF: sub_A0C5E764:off_A0C5E76Co
A082AD86 80 B5                     PUSH    {R7,LR}
A082AD88 FF F7 C3 FF               BL      sub_A082AD12
A082AD8C 01 1D                     ADD   R1, R0, #4
A082AD8E 07 20                     MOV   R0, #7
A082AD90 0D F0 CC EE               BLX   j_dwMODdw_0
A082AD94 08 06                     LSL   R0, R1, #0x18
A082AD96 00 0E                     LSR   R0, R0, #0x18
A082AD98 80 BD                     POP   {R7,PC}
如下,前面的其实是一个thunk。
A0838B2C
A0838B2C             ; Attributes: thunk
A0838B2C
A0838B2C             j_dwMODdw_0                           ; CODE XREF: sub_A082AC58+Cp
A0838B2C                                                   ; sub_A082ACEC+Ap ...
A0838B2C 04 F0 1F E5               LDR   PC, =dwMODdw
A0838B2C             ; End of function j_dwMODdw_0
A0838B2C
A0838B2C             ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
A0838B30 8C 94 60 A1 off_A0838B30    DCD dwMODdw             ; DATA XREF: j_dwMODdw_0r

剩下的就是网络信号了,可以从下面找到:
A08C0430
A08C0430             GetSignIconID                           ; CODE XREF: sub_A08BFE54+3Ap
A08C0430 70 B5                     PUSH    {R4-R6,LR}
A08C0432 2A 48                     LDR   R0, =0xA860E594
A08C0434 2A 4E                     LDR   R6, =0xFFFF
A08C0436 00 68                     LDR   R0,
A08C0438 00 28                     CMP   R0, #0
A08C043A 05 D1                     BNE   loc_A08C0448
A08C043C 25 48                     LDR   R0, byte_A08C04D4
A08C043E 00 88                     LDRH    R0,
A08C0440 C0 07                     LSL   R0, R0, #0x1F
A08C0442 01 D4                     BMI   loc_A08C0448
A08C0444 30 1C                     ADD   R0, R6, #0
A08C0446 70 BD                     POP   {R4-R6,PC}
A08C0448             ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
A08C0448
A08C0448             loc_A08C0448                            ; CODE XREF: GetSignIconID+Aj
A08C0448                                                   ; GetSignIconID+12j
A08C0448 26 4C                     LDR   R4, =word_A0A318B2
A08C044A 6D F1 D8 E8               BLX   GetNetSignValue
A08C044E 05 04                     LSL   R5, R0, #0x10
A08C0450 2D 0C                     LSR   R5, R5, #0x10
A08C0452 33 F2 CF FF               BL      sub_A0AF43F4
A08C0456 00 28                     CMP   R0, #0
A08C0458 07 D0                     BEQ   loc_A08C046A
A08C045A 33 F2 DF FF               BL      sub_A0AF441C
A08C045E 02 28                     CMP   R0, #2
A08C0460 01 D1                     BNE   loc_A08C0466
A08C0462 18 34                     ADD   R4, #0x18
A08C0464 01 E0                     B       loc_A08C046A
A08C0466             ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
A08C0466
A08C0466             loc_A08C0466                            ; CODE XREF: GetSignIconID+30j
A08C0466 1F 4C                     LDR   R4, =word_A0A318B2
A08C0468 0C 34                     ADD   R4, #0xC
A08C046A
A08C046A             loc_A08C046A                            ; CODE XREF: GetSignIconID+28j
A08C046A                                                   ; GetSignIconID+34j
从上面那个GetNetSignValue那个函数入手,经过一个Thunk后,到达:
A00EEADC
A00EEADC             sub_A00EEADC                            ; CODE XREF: GetNetSignValue+4j
A00EEADC                                                   ; DATA XREF: GetNetSignValue:off_A0A2D604o
A00EEADC 4F 48                     LDR   R0, =0xA86F0BB0;这个地址加8就是。
A00EEADE 80 68                     LDR   R0,
A00EEAE0 70 47                     BX      LR
A00EEAE0             ; End of function sub_A00EEADC
A00EEAE0
这里面的0xA86F0BB0,系统从它加8那里得到信号值。

完成了,天气太热了,不想抓图了!

devil_997 发表于 2005-8-15 00:01:00

支持楼主分享你的源文件..
这对学习补丁的移值有很大的帮助..

lijingwei 发表于 2005-8-15 00:02:00

文章是不是还没有完吧??

Bennie 发表于 2005-8-15 00:03:00

在继续中!!!!!!!!!!

devil_997 发表于 2005-8-15 00:04:00

现在对补丁的运作已经有一点了解,
但想提几个问题.....对于补丁需要调用一些数据,而在其它机型应该如果找到相应的地址,
之前读者过一些关于移值补丁的帖,,知道可以从"特征码"入手....可不可以..提供一些"特征码"出来呢..
每一种机型的地址应该都不同吧...
特别是..A8xxxxxx.这一类..在RAM中的地址......

lijingwei 发表于 2005-8-15 00:05:00

.....汗自己一个....看半天keil,不知道怎么用呢.

lijingwei 发表于 2005-8-15 00:06:00

....知道怎么用了....半天才发现.....

ShineGood 发表于 2005-8-15 00:07:00

一个小建议
#ifdef S6CV50
//for system function
const GETDATETIME getdate         = (GETDATETIME)0xA0AA6A53;
const GETWEEK getweek               = (GETWEEK)0xA082AD87;
const GetAkku getakku               = (GetAkku)0xA1254830;
const f_uint2str uint2str         = (f_uint2str)0xA0825687;
//const DWMODDW dwmoddw         = (DWMODDW)0xA160948C; //because Keil's problem

//for system data with Absolute Location.
word Akkpct __at 0xA863FF08;
word net         __at 0xA86F0BB8;
#endif
这种方法建议换成
#ifdef S6CV50
#include "S6CV50.h"
#endif
这样做的方法好处在
1. 在此基础上做多机型多版本移植时代码显得比较干净
2. 在S6Cv50.h中维护所有的地址即可,不用每次都根据程序所需的函数进行复制粘贴(在ADS中不会把多余无用的地址编译到目标中,Keil没试过,应该也不会吧)
3. 在手机版本升级后也方便根据该头文件更新

devil_997 发表于 2005-8-15 00:08:00

老大..想问一下..为什么生成的补丁会有
0x800078: 00000000000000000000000000000000
这样的一行??是由什么引起的??这应该要怎么去掉呢??

Bennie 发表于 2005-8-15 00:09:00

原帖由 ShineGood 于 2005-6-24 09:47 发表
一个小建议

这种方法建议换成

这样做的方法好处在
1. 在此基础上做多机型多版本移植时代码显得比较干净
2. 在S6Cv50.h中维护所有的地址即可,不用每次都根据程序所需的函数进行复制粘贴(在ADS中不会把多 ...
这个建议很好,开发时没有考虑这个问题。下次再发这些东西时会改过来!

Bennie 发表于 2005-8-15 00:10:00

原帖由 devil_997 于 2005-6-24 09:47 发表
老大..想问一下..为什么生成的补丁会有
0x800078: 00000000000000000000000000000000
这样的一行??是由什么引起的??这应该要怎么去掉呢??
那个是因为一些对齐规则引起的,不用管它,不要过于在乎这一点空间!

lijingwei 发表于 2005-8-15 00:11:00

问:
是怎么从工模的ACCU菜单 找到getakkuuint2str 和电池的百分比容量函数的?

Bennie 发表于 2005-8-15 00:12:00

找到ACCU菜单是通过他的显示的字符串。看了一下程序后发现,它是预先写好了整个字符串包括为数字预留的位置,使用的时候copy到buffer中,然后填充每一个位置。从这个发现了getakku函数,和uint2str函数,后面的memcpy是早就找到过的。百分比容量那个你找一下他在cap后面添的内容就知道了。

comerose 发表于 2005-8-15 00:13:00

呵呵!好东东!学习学习!

swat_lc 发表于 2005-8-15 00:14:00

keil还需要设置什么?我没法编译。提示
--- Error: can't execute 'ARM-ELF-AS'
(用楼主的项目文件打开)

cx65fan 发表于 2005-12-16 09:49:54

学习学习努力学习!!
页: [1]
查看完整版本: 如何用Keil ARM编译器作x65的Patch