1. 程式人生 > >vivi原始碼分析1

vivi原始碼分析1

通過vivi研究bootloader有一段時間了,基本是在與之相關的基礎方面做工作,還沒有真正深入研究vivi。以後的學習重心就要放到研究vivi原始碼上面了。我想,真正細緻地弄清楚vivi實現的細節,對C語言水平的提高,對ARM體系結構的認識,對S3C2410的熟悉,對嵌入式bootloader相關技術,都能有很大的好處。學習的進度會慢一些,但是務求深入,並且打好相關的基礎。

一、寫在前面的話
    嵌入式系統軟體開發主要包括五個方面:bootloader編寫(移植)、驅動程式編寫(移植)、作業系統裁減(移植)、檔案系統製作、應用程式編寫(移植)。嵌入式開發流程我已經熟悉,但是僅限於完成最為基本的工作,大部分是藉助網路資料,自己獨立解決的問題很有限。學習嵌入式系統已經一年了,算是入門了。然而,入門之後如何繼續深入學習嵌入式系統開發,如何提高自身的能力?
    我想,這也許是獨立摸索的學習者都會遇到的問題吧。思考之後有所得,核心就是一句話:務實,理論與實踐結合!具體說來,就是要不斷的認識自己,去了解自己最適合做什麼。這是最為重要的,如果不知道做什麼,就無法安排學習的重點。嵌入式開發的領域太廣,要想在方方面面都深入不太容易(當然牛人除外)。現在對自己的認識如下:本科有硬體、通訊背景,但是沒有太多機會進行硬體設計。而硬體設計最為重要的就是經驗,動手能力,所以不打算把硬體設計作為學習的重點。底層軟體開發既需要對硬體熟悉,又需要軟體設計能力,正適合我。所以以後的學習,以底層軟體開發(bootloader設計、驅動程式設計)為重點,同時也要加強硬體學習。學習有重點,但是嵌入式開發的其他領域也要涉及,瞭解廣博才能更有助於設計。進展慢不要緊,關鍵是要深入,深入,再深入。真正地去理解這些技術,並且能夠熟練的應用。這半年的核心就是bootloader技術研究,打算先看vivi,然後看uboot。手頭上的板子有s3c2410、at91rm9200,這些都可以拿來訓練,爭取能夠通過bootloader技術的掌握,同時熟悉了ARM體系結構、ARM彙編、開發工具等等一系列相關內容,總結學習的方法,提高學習能力。 二、準備工作     在分析vivi原始碼的時候,不打算完全按照vivi的程式碼來進行。我的思路是,以從nand flash啟動為主線,分析從上電到引導核心的過程。以自己的理解去實現vivi的原始碼,要自己手動編寫,即使與vivi的程式碼相同。只有這樣,才能從整體上理解vivi的設計方法,理解vivi各個功能的實現細節。這份文件算是自己的學習筆記,儘可能詳細,同時希望有研究vivi的朋友一起交流,我的email:
[email protected]
。 三、bootloader stage1:【arch/s3c2410/head.S】     首先解決一個問題,就是為什麼使用head.S而不是用head.s?有了GNU AS和GNU Gcc的基礎,不難理解主要原因就是為了使用C前處理器的巨集替換和檔案包含功能(GNU AS的預處理無法完成此項功能)。可以參考前面的總結部分。這樣的好處就是可以使用C前處理器的功能來提高ARM彙編的程式設計環境,更加方便。但是因為ARM彙編和C在巨集替換的細節上有所不同,為了區分,引入了__ASSEMBLY__這個變數,這是通過Makefile中AFLAGS來引入的,具體如下:

AFLAGS :

= -D__ASSEMBLY__ $(CPPFLAGS)

    在後面的標頭檔案中,會看到很多#ifdef __ASSEMBLY__等的操作,就是用來區分這個細節的。在編譯彙編檔案時,加入AFLAGS選項,所以__ASSEMBLY__傳入,也就是定義了__ASSEMBLY__;在編譯C檔案時,沒有用AFLAGS選項,自然也就沒有定義__ASSEMBLY__。由此相應的問題就比較清晰了。這個小技巧也是值得學習和借鑑的。 1 首先關注一下開始的三個標頭檔案。

#include "config.h"
#include "linkage.h"


#include "machine.h"

(1)利用source insight來檢視【include/config.h】。

#ifndef _CONFIG_H_
#define _CONFIG_H_

#include "autoconf.h"

#endif /* _CONFIG_H_ */

    可見,config.h只是包含一個autoconf.h。而關於autoconf.h的生成,在vivi配置檔案分析的時候也解釋的很清楚了,在這裡就不用再細分析了。需要解釋的一點是,如果寫一個專用的bootloader,不採用vivi的配置機制,那麼配置部分就沒有這麼複雜了,只需要在include資料夾中包含一個配置標頭檔案即可。現在bootloader的設計有兩種趨勢,一種是針對特定應用,有特殊要求,也就是“專用”。那麼設計時,不需要過多的配置,只需要簡單的完成引導核心的功能就可以了。二是普通應用,一般是對基本“通用”的bootloader,比如uboot等,然後根據相應的模版進行移植。這就需要了解uboot等的架構,可以進行定製和功能的增加。uboot完成的不僅僅是一個bootloader的功能,還可以提供除錯等功能,所以其角色還包含駐留程式這個功能,也就是uboot真正的角色是monitor。當然,可以不加區分,統稱為bootloader。而分析vivi原始碼的實現,對這兩個方向都有幫助。 (2)【include/linkage.h】就是實現了ENTRY巨集的封裝。其實這個標頭檔案也僅僅為head.S提供了服務,實際上沒有必要寫的這麼複雜,可以簡化一些。比如,我修改了這個標頭檔案,如下:

[[email protected] include]$ cat linkage.h
#ifndef _VIVI_LINKAGE_H
#define _VIVI_LINKAGE_H

#define SYMBOL_NAME(X) X
#ifdef __STDC__
        #define SYMBOL_NAME_LABEL(X) X##:
#else
        #define SYMBOL_NAME_LABEL(X) X/**/:
#endif

#ifdef __ASSEMBLY__

#define ALIGN .align 0

#define ENTRY(name) /
        .globl SYMBOL_NAME(name); /
        ALIGN; /
        SYMBOL_NAME_LABEL(name)

#endif

#endif

    在這裡,要加強一下C語言巨集的設計和分析能力。下面就幾個點簡單的分析一下,後面專門就C巨集部分做個總結。     關於__STDC__這個巨集,是編譯器自動新增的,含義就是支援標準C。如果支援標準C,那麼##的作用就是“連線”,所以SYMBOL_NAME_LABEL(_start)巨集展開為_start:,如果不支援標準C,則利用了C前處理器對註釋的處理方式,就是把/**/替換為一個空格,可以測試一下。     另外,關於ENTRY巨集的封裝,利用了GNU AS在ARM上的相關特點。首先,利用了分號作為三條語句的連線符,而分號是GNU AS彙編註釋符的一種(另外一種是@)。另外,關於ALIGN為什麼用.align 0。這可以參考GNU AS手冊,上面講解的比較清晰,主要是為了相容ARM本身的編譯器。理解了這個也就不難得出ENTRY(_start)巨集展開後的形式了。有一個技巧就是可以通過下面的命令來檢測巨集展開後的結果,比如:

[[email protected] vivi_myboard]# gcc -E -D__ASSEMBLY__ -I./include arch/s3c2410/head.S >aaa

可以檢視aaa檔案的顯示結果,做了一些註釋:

# 1 "arch/s3c2410/head.S"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "arch/s3c2410/head.S"
# 35 "arch/s3c2410/head.S"
# 1 "include/config.h" 1
# 14 "include/config.h"
# 1 "include/autoconf.h" 1
# 15 "include/config.h" 2
# 36 "arch/s3c2410/head.S" 2
# 1 "include/linkage.h" 1
# 37 "arch/s3c2410/head.S" 2
# 1 "include/machine.h" 1
# 19 "include/machine.h"
# 1 "include/platform/smdk2410.h" 1

# 1 "include/s3c2410.h" 1
# 22 "include/s3c2410.h"
# 1 "include/hardware.h" 1
# 23 "include/s3c2410.h" 2
# 1 "include/bitfield.h" 1
# 24 "include/s3c2410.h" 2
# 3 "include/platform/smdk2410.h" 2

# 1 "include/sizes.h" 1
# 8 "include/platform/smdk2410.h" 2
# 74 "include/platform/smdk2410.h"
# 1 "include/architecture.h" 1
# 75 "include/platform/smdk2410.h" 2
# 20 "include/machine.h" 2
# 38 "arch/s3c2410/head.S" 2

@ Start of executable code

巨集定義展開
.globl _start; .align 0; _start:
.globl ResetEntryPoint; .align 0; ResetEntryPoint:

下面是裝載中斷向量表,ARM規定,在起始必須有8條跳轉指令,你可以用b,也可以用ldr pc,檔名。這樣的8條規則的標誌被arm定義為bootloader的識別標誌,檢測到這樣的標誌後,就可以從該位置啟動。這樣的做法是因為開始的時候不一定有bootloader,必須有一種識別機制,如果識別到bootloader,那麼就從bootloader啟動。
@
@ Exception vector table (physical address = 0x00000000)
@

@ 0x00: Reset
        b Reset

@ 0x04: Undefined instruction exception
UndefEntryPoint:
        b HandleUndef

@ 0x08: Software interrupt exception
SWIEntryPoint:
        b HandleSWI

@ 0x0c: Prefetch Abort (Instruction Fetch Memory Abort)
PrefetchAbortEnteryPoint:
        b HandlePrefetchAbort

@ 0x10: Data Access Memory Abort
DataAbortEntryPoint:
        b HandleDataAbort

@ 0x14: Not used
NotUsedEntryPoint:
        b HandleNotUsed

@ 0x18: IRQ(Interrupt Request) exception
IRQEntryPoint:
        b HandleIRQ

@ 0x1c: FIQ(Fast Interrupt Request) exception
FIQEntryPoint:
        b HandleFIQ

下面是固定位置存放環境變數
@
@ VIVI magics
@

@ 0x20: magic number so we can verify that we only put
        .long 0
@ 0x24:
        .long 0
@ 0x28: where this vivi was linked, so we can put it in memory in the right place
        .long _start
@ 0x2C: this contains the platform, cpu and machine id
        .long ((1 << 24) | (6 << 16) | 193)
@ 0x30: vivi capabilities
        .long 0

@ 0x34:
        b SleepRamProc

@
@ Start VIVI head
@
Reset:

        @ disable watch dog timer
        mov r1, #0x53000000
        mov r2, #0x0
        str r2, [r1]
# 121 "arch/s3c2410/head.S"
        @ disable all interrupts
        mov r1, #0x4A000000
        mov r2, #0xffffffff
        str r2, [r1, #0x08]
        ldr r2, =0x7ff
        str r2, [r1, #0x1C]

        @ initialise system clocks
        mov r1, #0x4C000000
        mvn r2, #0xff000000
        str r2, [r1, #0x00]

        @ldr r2, mpll_50mhz
        @str r2, [r1, #0x04]

        @ 1:2:4
        mov r1, #0x4C000000
        mov r2, #0x3
        str r2, [r1, #0x14]

        mrc p15, 0, r1, c1, c0, 0 @ read ctrl register
        orr r1, r1, #0xc0000000 @ Asynchronous
        mcr p15, 0, r1, c1, c0, 0 @ write ctrl register

        @ now, CPU clock is 200 Mhz
        mov r1, #0x4C000000
        ldr r2, mpll_200mhz
        str r2, [r1, #0x04]
# 164 "arch/s3c2410/head.S"
        bl memsetup

        @ Check if this is a wake-up from sleep
        ldr r1, PMST_ADDR
        ldr r0, [r1]
        tst r0, #((1 << 1))
        bne WakeupStart

        @ All LED on
        mov r1, #0x56000000
        add r1, r1, #0x50
        ldr r2,=0x55aa
        str r2, [r1, #0x0]
        mov r2, #0xff
        str r2, [r1, #0x8]
        mov r2, #0x00
        str r2, [r1, #0x4]
# 230 "arch/s3c2410/head.S"
        @ set GPIO for UART
        mov r1, #0x56000000
        add r1, r1, #0x70
        ldr r2, gpio_con_uart
        str r2, [r1, #0x0]
        ldr r2, gpio_up_uart
        str r2, [r1, #0x8]
        bl InitUART
# 259 "arch/s3c2410/head.S"
        bl copy_myself

        @ jump to ram
        ldr r1, =on_the_ram
        add pc, r1, #0
        nop
        nop
1: b 1b @ infinite loop

on_the_ram:
# 279 "arch/s3c2410/head.S"
        @ get read to call C functions
        ldr sp, DW_STACK_START @ setup stack pointer
        mov fp, #0 @ no previous frame, so fp=0
        mov a2, #0 @ set argv to NULL

        bl main @ call main

        mov pc, #0x00000000 @ otherwise, reboot

@
@ End VIVI head
@

下面是子例程
@
@ Wake-up codes
@

WakeupStart:
        @ Clear sleep reset bit
        ldr r0, PMST_ADDR
        mov r1, #(1 << 1)
        str r1, [r0]

        @ Release the SDRAM signal protections
        ldr r0, PMCTL1_ADDR
        ldr r1, [r0]
        bic r1, r1, #((1 << 19) | (1 << 18) | (1 << 17))
        str r1, [r0]

        @ Go...
        ldr r0, PMSR0_ADDR @ read a return address
        ldr r1, [r0]
        mov pc, r1
        nop
        nop
1: b 1b @ infinite loop

SleepRamProc:
        @ SDRAM is in the self-refresh mode */
        ldr r0, REFR_ADDR
        ldr r1, [r0]
        orr r1, r1, #(1 << 22)
        str r1, [r0]

        @ wait until SDRAM into self-refresh
        mov r1, #16
1: subs r1, r1, #1
        bne 1b

        @ Set the SDRAM singal protections
        ldr r0, PMCTL1_ADDR
        ldr r1, [r0]
        orr r1, r1, #((1 << 19) | (1 << 18) | (1 << 17))
        str r1, [r0]


        ldr r0, PMCTL0_ADDR
        ldr r1, [r0]
        orr r1, r1, #(1 << 3)
        str r1, [r0]
1: b 1b
# 379 "arch/s3c2410/head.S"
.globl memsetup; .align 0; memsetup:
        @ initialise the static memory

        @ set memory control registers
        mov r1, #0x48000000
        adrl r2, mem_cfg_val
        add r3, r1, #52
1: ldr r4, [r2], #4
        str r4, [r1], #4
        cmp r1, r3
        bne 1b

        mov pc, lr



@
@ copy_myself: copy vivi to ram
@
copy_myself:
        mov r10, lr

        @ reset NAND
        mov r1, #0x4E000000
        ldr r2, =0xf830 @ initial value
        str r2, [r1, #0x00]
        ldr r2, [r1, #0x00]
        bic r2, r2, #0x800 @ enable chip
        str r2, [r1, #0x00]
        mov r2, #0xff @ RESET command
        strb r2, [r1, #0x04]
        mov r3, #0 @ wait
1: add r3, r3, #0x1
        cmp r3, #0xa
        blt 1b
2: ldr r2, [r1, #0x10] @ wait ready
        tst r2, #0x1
        beq 2b
        ldr r2, [r1, #0x00]
        orr r2, r2, #0x800 @ disable chip
        str r2, [r1, #0x00]

        @ get read to call C functions (for nand_read())
        ldr sp, DW_STACK_START @ setup stack pointer
        mov fp, #0 @ no previous frame, so fp=0

        @ copy vivi to RAM
        ldr r0, =(0x30000000 + 0x04000000 - 0x00100000)
        mov r1, #0x0
        mov r2, #0x20000
        bl nand_read_ll

        tst r0, #0x0
        beq ok_nand_read
# 441 "arch/s3c2410/head.S"
ok_nand_read:

        @ verify
        mov r0, #0
        ldr r1, =0x33f00000
        mov r2, #0x400 @ 4 bytes * 1024 = 4K-bytes
go_next:
        ldr r3, [r0], #4
        ldr r4, [r1], #4
        teq r3, r4
        bne notmatch
        subs r2, r2, #4
        beq done_nand_read
        bne go_next
notmatch:
# 469 "arch/s3c2410/head.S"
1: b 1b
done_nand_read:

        mov pc, r10

@ clear memory
@ r0: start address
@ r1: length
mem_clear:
        mov r2, #0
        mov r3, r2
        mov r4, r2
        mov r5, r2
        mov r6, r2
        mov r7, r2
        mov r8, r2
        mov r9, r2

clear_loop:
        stmia {r2-r9}
        subs r1, r1, #(8 * 4)
        bne clear_loop


        mov pc, lr
# 613 "arch/s3c2410/head.S"
@ Initialize UART
@
@ r0 = number of UART port
InitUART:
        ldr r1, SerBase
        mov r2, #0x0
        str r2, [r1, #0x08]
        str r2, [r1, #0x0C]
        mov r2, #0x3
        str r2, [r1, #0x00]
        ldr r2, =0x245
        str r2, [r1, #0x04]

        mov r2, #((50000000 / (115200 * 16)) - 1)
        str r2, [r1, #0x28]

        mov r3, #100
        mov r2, #0x0
1: sub r3, r3, #0x1
        tst r2, r3
        bne 1b
# 653 "arch/s3c2410/head.S"
        mov pc, lr


@
@ Exception handling functions
@
HandleUndef:
1: b 1b @ infinite loop

HandleSWI:
1: b 1b @ infinite loop

HandlePrefetchAbort:
1: b 1b @ infinite loop

HandleDataAbort:
1: b 1b @ infinite loop

HandleIRQ:
1: b 1b @ infinite loop

HandleFIQ:
1: b 1b @ infinite loop

HandleNotUsed:
1: b 1b @ infinite loop

@
@ Low Level Debug
@
# 838 "arch/s3c2410/head.S"
@
@ Data Area
@
@ Memory configuration values
.align 4
mem_cfg_val:
        .long 0x22111110
        .long 0x00000700
        .long 0x00000700
        .long 0x00000700
        .long 0x00000700
        .long 0x00000700
        .long 0x00000700
        .long 0x00018005
        .long 0x00018005
        .long 0x008e0459
        .long 0xb2
        .long 0x30
        .long 0x30

@ Processor clock values
.align 4
clock_locktime:
        .long 0x00ffffff
mpll_50mhz:
        .long ((0x5c << 12) | (0x4 << 4) | (0x2))

mpll_200mhz:
        .long ((0x5c << 12) | (0x4 << 4) | (0x0))
clock_clkcon:
        .long 0x0000fff8
clock_clkdivn:
        .long 0x3
@ initial values for serial
uart_ulcon:
        .long 0x3
uart_ucon:
        .long 0x245
uart_ufcon:
        .long 0x0
uart_umcon:
        .long 0x0
@ inital values for GPIO
gpio_con_uart:
        .long 0x0016faaa
gpio_up_uart:
        .long 0x000007ff

        .align 2
DW_STACK_START:
        .word (((((0x30000000 + 0x04000000 - 0x00100000) - 0x00100000) - 0x00004000) - (0x00004000 + 0x00004000 +

0x00004000)) - 0x00008000)+0x00008000 -4
# 922 "arch/s3c2410/head.S"
.align 4
SerBase:

        .long 0x50000000
# 935 "arch/s3c2410/head.S"
.align 4
PMCTL0_ADDR:
        .long 0x4c00000c
PMCTL1_ADDR:
        .long 0x56000080
PMST_ADDR:
        .long 0x560000B4
PMSR0_ADDR:
        .long 0x560000B8
REFR_ADDR:
        .long 0x48000024
[[email protected] vivi_myboard]#

    【include/machine.h】則是利用條件編譯來選擇適合自己開發板的標頭檔案,本開發板的標頭檔案是【include/platform/smdk2410.h】,主要是一些暫存器的初始值(以v開頭)和一些相關的地址等等的定義。一般開發板不同,都是修改此檔案相應的部分。 2、關於中斷向量表     開始對中斷向量表很疑惑。現在的理解比較清晰了,在硬體實現上,會支援中斷機制,這個可以參考微機介面原理部分詳細理解。現在的中斷機制處理的比較智慧,對每一種中斷會固定一箇中斷向量,比如說,發生IRQ中斷,中斷向量地址為0x00000018(當然,這還與ARM9TDMI core有關,其中一個引腳可以把中斷向量表配置為高階啟動,或者低端啟動。你可以通過CP15的register 1的bit 13的V bit來設定,可以檢視Datasheet TABLE 2-10來解決。),那麼PC要執行的指令就是0x00000018。如果在這個地址放上一個跳轉指令(只能使用b或者ldr),那麼就可以跳到實際功能程式碼的實現區了。ARM體系結構規定在上電覆位後的起始位置,必須有8條連續的跳轉指令,這是bootloader的識別入口,通過硬體實現。看一下vivi的中斷向量表:

@ 0x00
        b Reset
@ 0x04
HandleUndef:
        b HandleUndef
@ 0x08
HandleSWI:
        b HandleSWI
@ 0x0c
HandlePrefetchAbort:
        b HandlePrefetchAbort
@ 0x10
HandleDataAbort:
        b HandleDataAbort
@ 0x14
HandleNotUsed:
        b HandleNotUsed
@ 0x18
HandleIRQ:
        b HandleIRQ
@ 0x1c
HandleFIQ:
        b HandleFIQ

    注意,中斷向量表可以修改,也可以通過MMU實現地址重對映,來改變對應的物理介質。如果不對每種中斷進行測試,可以採用下面的書寫方式。


@ 0x00
        b Reset
@ 0x04
HandleUndef:
        b .
@ 0x08
HandleSWI:
        b .
@ 0x0c
HandlePrefetchAbort:
        b .
@ 0x10
HandleDataAbort:
        b .
@ 0x14
HandleNotUsed:
        b .
@ 0x18
HandleIRQ:
        b .
@ 0x1c
HandleFIQ:
        b .

    其中,“.”表示當前地址,那麼很明顯,“b .”,就表示了死迴圈了。     如果增加中斷處理,則需要考慮使用b還是使用ldr。主要的區別在於b指令跳轉範圍有限,僅僅是+-32M。所以如果超出32M,那麼就要採用ldr了,基本的模式是:

.globl _start
_start: b reset
        ldr pc, _undefined_instruction
        ldr pc, _software_interrupt
        ldr pc, _prefetch_abort
        ldr pc, _data_abort
        ldr pc, _not_used
        ldr pc, _irq
        ldr pc, _fiq

_undefined_instruction: .word undefined_instruction
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq
_fiq: .word fiq

...

/*
 * exception handlers
 */
        .align  5
undefined_instruction:
        get_bad_stack
        bad_save_user_regs
        bl      do_undefined_instruction

...

    這就是uboot採用的方式。這些技術是相當成熟的。現在在加速啟動上,就會考慮對中斷向量表作出一些相應的處理。比如,如果從nor flash啟動,啟動後把kernel等載入到sdram裡面執行。無疑,sdram裡面執行速度比nor flash裡面快。但是如果不對中斷向量表進行處理,那麼發生中斷時,首先還是得訪問nor flash裡面的中斷向量表。然後跳轉到sdram相應的執行部分。所以,提高速度就是要解決不經過nor flash,完全在sdram裡面執行。採用的技術手段是通過重定向機制,有些SoC提供了這樣的硬體手段,但是有些沒有,比如S3C2410就沒有。但是S3C2410有MMU,可以通過MMU來改變對映關係實現雖然中斷向量還是0x00000018,但是實際的物理介質是sdram,具體的方法可以參考MMU部分的總結來完成。如果採用nand flash啟動的話,就不必作出這樣的處理。本質上是因為nand flash並非記憶體對映,而且中斷向量表佔用的是開始4K的steppingstone,是sram,其速度比sdram還要快,如果作出上面的處理,速度反而會下降了。     中斷向量表的出現也是具有歷史原因的,是解決特定問題採用的一種技術手段。為了瞭解中斷向量表的一些特殊用途,還應該理解載入域和執行域的不同。這樣就能從全域性上把握處理的原則,相應的解決機制也比較容易理解了。 3、關於vivi magic number     接下來,vivi設定了自己的一些magic number。理解什麼是magic number,需要通過google來查詢。本來wiki上有關於magic number(programming)的解釋,不過最近打不開了,找了一個替代網站,網址是http://www.answers.com/,上面的內容是wiki的備份吧(好像可以這樣理解)。關於magic number(programming)的文章為http://www.answers.com/topic/magic-number-programming。關於magic number(programming)的總結和解釋,專門拿出一篇來總結(基本上是上述英文文章的翻譯吧,不過講解的確實非常清晰,讀完之後對magic number就很明白了,而且在程式設計中,如何恰當的使用magic number,也會有一定的認識)。

@ 0x20: magic number so we can verify that we only put
        .long 0
@ 0x24:
        .long 0
@ 0x28