Android Porting and Qemu

1 Introduction 


Google explains that Android is a software stack for mobiledevices that includes an operating system, middleware and keyapplications. This document explains the Android architecture byGoogle and porting procedure on the real hardware. The explanationis based on the m3 sdk version of the Android emulator.


If you have enough knowledge about patching the kernel,resolving rejections from a patch, making an ramdisk image, and theLinux kernel itself, reading this article will be easier.

2 Copyrightand Acknowledgements 


This document is copyright (c) Kwangwoo Lee (kwangwoo.lee atgmail dot com). Permission is granted to copy, distribute and/ormodify this document under the terms of the GNU Free DocumentationLicense.

AndroidPortingOnRealTarget/ko  -Korean translation by dasomoli (dasomoli at gmail dot com).

3 Thebrief analysis of the Androidarchitecture 


3.1 AndroidKernel 


The most different things are the Android kernel uses ARMEABI(Embedded Application Binary Interface)and  OpenBinder  IPC(InterProcess Communication). If you want to compile the kernelsupporting ARM EABI, you should rebuild toolchains to support ARMEABI.

The Android sdk emulates goldfish architecture using qemu. Thealsa may be used for audio on Android. See the audio.c file in thegoldfish architecture directory and the driver uses /dev/eac foraudio on the Android system. RTC(Real Time Clock) device is alsoused through /dev/rtc0.

The following parts explain the main differences:

3.1.1 ARMEABI 


EABI is the new "Embedded" ABI by ARM Ltd. The changes arelisted on Debian wiki. ( http://wiki.debian.org/ArmEabiPort)

Example with long ftruncate64(unsigned int fd, loff_t length):

legacy ABI:
- put fd into r0
- put length into r1-r2
- use "swi #(0x900000 + 194)" to call the kernel

new ARM EABI:
- put fd into r0
- put length into r2-r3 (skipping over r1)
- put 194 into r7
- use "swi 0" to call the kernel

The Android uses EABI kernel feature. Enable kernel options ofthe CONFIG_AEABI and CONFIG_OABI_COMPAT. You can see thedifferences of the executable binary as follows :

  • Legacy ABI
$ arm-softfloat-linux-gnu-objdump -x t7-demo | grep private
private flags = 202: [APCS-32] [FPA float format] [software FP] [has entry point]

$ file t7-demo
t7-demo: ELF 32-bit LSB executable, ARM, version 1 (ARM), 
for GNU/Linux 2.4.3, dynamically linked (uses shared libs), 
for GNU/Linux 2.4.3, stripped

  • ARM EABI
$ arm-softfloat-linux-gnueabi-objdump -x t7-demo  | grep private
private flags = 4000002: [Version4 EABI] [has entry point]

$ file t7-demo
t7-demo: ELF 32-bit LSB executable, ARM, version 1 (SYSV), 
for GNU/Linux 2.6.14, dynamically linked (uses shared libs), 
for GNU/Linux 2.6.14, stripped


What is the ABI for the ARM Architecture? Is it the same asthe ARM EABI?

The ABI for the ARM Architecture is a standard developed byARM and its partners (including CodeSourcery) that explains howcompilers, assemblers, linkers, and other similar tools shouldgenerate object files and executable files. Tools that correctlyimplement the ABI for the ARM Architecture can interoperate; i.e.,objects files built with one toolchain can be combined with objectfiles built with another toolchain if both compilers use the ABIfor the ARM Architecture. The "ARM EABI" is an informal name forthe ABI for the ARM Architecture.

3.1.2 OpenBinder 


The  OpenBinder  providesa object-oriented operating system environment. It is designed tobe hosted by traditional kernels. This project is started at Be.Inc. as the part of the next generation BeOS, and finishedimplementing at  PalmSource  asa core part at the Cobalt system.

It is a system oriented component architecture rather thanapplication oriented, and It provides IPC between processes,threadpool, memory management and clean up feature at the end ofreference of an binder object.

The vanilla kernel do not have  OpenBinder  IPCmechanism you should patch the kernel.The  OpenBinder  offersthread management for the system through /dev/binder. It is thereason that Android system do not offer thread libraries.

After patching the kernel, you can see the files for binder atdrivers/binder/.

3.1.3 FrameBuffer 


The basic frame buffer driver should be implemented already.After that you need to implement the differences between yourarchitecture driver and the goldfish driver.

The frame buffer driver of the goldfish architecture supportsthe fb_pan_display function of the struct fb_ops. It means youshould allocate memory twice rather than the actual framesize.

  • Initialize frame buffer information
struct fb_info *fbinfo;
...
fbinfo->fix.ypanstep     = 1;
fbinfo->var.yres_virtual    = gm->lcd.yres * 2;
fbinfo->fix.smem_len        =    (gm->lcd.xres *
                                gm->lcd.yres *
                                gm->lcd.bpp / 8) * 2;

  • Allocate frame buffer memory
struct mvfb_info *fbi;
...
fbi->map_size = PAGE_ALIGN(fbi->fb->fix.smem_len + PAGE_SIZE);
fbi->map_cpu  = dma_alloc_writecombine(fbi->dev, fbi->map_size,
                                       &fbi->map_dma, GFP_KERNEL);

  • Implement fb_pan_display fuction hook
static int mvfb_pan_display(struct fb_var_screeninfo *var, struct fb_info *fb)
{
...
}

static struct fb_ops mvfb_ops = {
        .owner          = THIS_MODULE,

        .fb_check_var   = mvfb_check_var,
        .fb_set_par     = mvfb_set_par, 
        .fb_setcolreg   = mvfb_setcolreg,
        .fb_blank       = mvfb_blank,
        .fb_pan_display = mvfb_pan_display,

        .fb_fillrect    = cfb_fillrect,
        .fb_copyarea    = cfb_copyarea,
        .fb_imageblit   = cfb_imageblit,

        .fb_mmap        = mvfb_mmap,    
};

The device file is located at /dev/graphics/fb0.

3.1.4 InputDevices 


Android uses event device for user input. There are threedevices such as keypad, qwerty2 keyboard and mouse. The qwerty2keyboard and mouse are normal devices. So I just explain the keypadand touchscreen which mouse device is replaced with.

On the Android shell, Cat the/proc/bus/input/{devices,handlers} and then you will see thedevices used for the Android.
$ adb shell

# cat /proc/bus/input/devices
I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="goldfish-events-keyboard"
P: Phys=
S: Sysfs=/class/inut/input0
U: Uniq=
H: Handlers=kbd mouse0 event0
...
#
# cat /proc/bus/input/handlers
N: Number=0 Name=kbd
N: Number=1 Name=mousedev Minor=32
N: Number=2 Name=evdev Minor=64
#

  • Keypad 

Qemu emulates goldfish-events-keyboard. It is a keypad using eventdevice(/dev/input/event0). So you should know which key event andvalues come from the event device to activate Android applications.To do that, read event0 device with cat and redirect the output toa file. If you push and release the key button on emulator, theoutput values will be saved.

The output format is struct input_event. So the output on eachevent is 16 bytes like 8 bytes for time, 2 bytes for type, 2 bytesfor code, 4 bytes for value. Read input.txt andinput-programming.txt about input event devices in theDocumentation/input directory of the Linux kernel sourcecode.

struct input_event {
        struct timeval time;
        unsigned short type;
        unsigned short code;
        unsigned int value;
};

The Tiger7 evaluation board has it's own scancode table. Thefollowing shows the key layout on evaluation board, scancode table,and Android keycodes:
static unsigned short android_keycode[] = {
        
        KEY_HOME,         KEY_UP,       KEY_BACK,
        KEY_LEFT,         KEY_REPLY,    KEY_RIGHT,
        KEY_SEND,         KEY_DOWN,     KEY_END,
        KEY_KBDILLUMDOWN, KEY_RESERVED, KEY_PLAY
};

There is a power button on emulator, but I skipped it to getoutput value.

If an interrupt of the keypad is caught, translate thescancode with the keycode of the Android on the above table andsend event to user space application.
...
keycode = translate_keycode(scancode);
...
input_event(keydev->input, EV_KEY, keycode, KEY_PRESSED);
or
input_event(keydev->input, EV_KEY, keycode, KEY_RELEASED);
...

The high resolution timer - hrtimer is used for reduce keypaddebounce.

  • Touchscreen 

If you have a touchscreen driver supporting the event interface fora pointing device, it'll work well. If you do not have it, you mayimplement it or use other pointing devices. Fortunately theevaluation board has already implemented touchscreen driver -drivers/input/touchscreen/tsc2007.c - which is made just beforebeginning to porting Android. Refer the drivers ondrivers/input/touchscreen/ to implement your own driver and thetext files on Documentation/input/.

Here is the output of the /proc/bus/input/{devices,handlers}on evaluation board.
# cat /proc/bus/input/devices
I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="MVT7 KEYPAD"
P: Phys=
S: Sysfs=/class/input/input0
U: Uniq=
H: Handlers=kbd event0 evbug
B: EV=f
...

I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="TSC2007 Touchscreen"
P: Phys=0-0090/input0
S: Sysfs=/class/input/input1
U: Uniq=
H: Handlers=event1 evbug
B: EV=b
B: KEY=400 0 0 0 0 0 0 0 0 0 0
B: ABS=1000003

# cat /proc/bus/input/handlers
N: Number=0 Name=kbd
N: Number=1 Name=evdev Minor=64
N: Number=2 Name=evbug

As a result, the keypad uses /dev/input/event0 and thetouchscreen interface uses /dev/input/event1 on applicationlayer.

3.1.5 LowMemory Killer 


The Linux Kernel has an OOM(Out of Memory) killer for thesituation that no memory is left to allocate for a request of aprocess. It examines all processes and keeps score with somerestrictions. The process with highest score will be killed exceptinit.

The Low Memory Killer of the Android behaves a bit differentagainst OOM killer. It classifies processes according to theimportance with groups and kills the process in the lowest group.It will make the system to be stable at the view of the end users.For example, the UI Process - foreground process - is the mostimportant process for the end users. So to keep the process livelooks more stable than keeping other background processeslive.

Enable CONFIG_LOW_MEMORY_KILLER after patching the kernel.

3.1.6 AndroidLogger 


If you enable this feature, you can see some usefulinformation about Android through /dev/log/main. There are threedevice files on /dev/log such as main, events, radio. The/dev/log/radio file seems to be related with a modem device and rildaemon - rild - on Android system.

When this logger is enabled, the system performance is a bitslower on the system. To use this feature, enableCONFIG_ANDROID_LOGGER.

3.1.7 AndroidPower 


The Android power is for the battery management on devices andsome subsystem related with power management like inotify featureon file system. It is not necessary to start up Android through theinit( ShellScript) of the Androidsystem. But the runtime binary looks up some files regardingAndroid power - /sys/android_power/acruire_partial_wake_lock - onstarting up Android manually and failed to start up. EnableCONFIG_ANDROID_POWER to use.
- 예전 버전의 문서에서 init은 바이너리로 되어있었는데 쉘스크립트로 바뀌어 있네요. 이전 문서에서 말한 init은안드로이드 램디스크의 루트 디렉토리 밑에 있는 init 바이너리를 말씀하신 것 같은데, 그 것이 아니고 다른init인건가요? 아니면 그 init이 쉘 스크립트인 것인가요? --dasomoli 

- 문서를 작성한 m3 버전에서는 binary 였습니다. 다른 분이 shell script로 바꾼 것 같네요. 번역해주셔서 감사합니다. --  이광우 

- m5 버전에서도 바이너리인 것 같아서요. 그리고 별 말씀을요.^^; -- dasomoli

3.1.8 PanicTimeout 


It is not necessary to start up Android on evaluation board.Set CONFIG_PANIC_TIMEOUT with a desired value.

3.2 AndroidRoot File system 


Android emulator has 3 basic images on tools/lib/imagesdirectory.

  • ramdisk.img
  • system.img
  • userdata.img 

ramdisk.img is gziped cpio archive. ramdisk image is very small andcontains configuration files, and some executable files such asinit and recovery. The init file is not a regular system V init. Itis made just for the Android and do special things to start up theAndroid system.

system.img and userdata.img are VMS Alpha executable.system.img and userdata.img have the contents of /system and /datadirectory on root file system. They are mapped on NAND devices withyaffs2 file system. /dev/block/mtdblock0 for /system and/dev/block/mtdblock1 for /data.

/system directory has libraries and default system packages(*.apk). /data directory has timezone, cache,and  ApiDemos.apk package.

The main services are zygote(/system/bin/app_process),runtime(/system/bin/runtime), and dbus(/system/bin/dbus-daemon).You can see the /etc/init.rc file on the Android ramdiskimage.

...
zygote {
    exec /system/bin/app_process
    args {
        0 -Xzygote
        1 /system/bin
        2 --zygote
    }
    autostart 1
}
runtime {
    exec /system/bin/runtime
    autostart 1
}
...
dbus {
    exec /system/bin/dbus-daemon
    args.0 --system
    args.1 --nofork
    autostart 1
}
...

3.3 Licensesof the Android Packages 


tools/lib/images/NOTICE contains package lists and licensesfor each libraries. The table of the licenses is cited from thepresentation by Lim, GeunSik  at2008 Korea Android Summit.

Open SourceLicense
Linux KernelGPL
NetBSD CLibraryBSD
DBUSGPL2
OpenBinder (core)GPL2
YAFFS2GPL
SQLiteGPL2
WebkitBSD (including LGPL)
WebCoreLGPL
SDLLGPL
SGLGoogle(Skia)
OpenGLSGI OpenGL (BSD/MPL)


4 Toolchainsupporting ARM EABI 


The toolchain represents the tools to be used for the systemdevelopment. It contains C/C++ compiler, linker, libraries,binutils, and etc. The Android kernel and system requires EABIsupport. So legacy toolchain is not compatible to make the Androidsystem.

4.1 Buildingtoolchain 


To make life easier, I used the crosstool-0.43 script( http://www.kegel.com/crosstool/) byDan Kegel. Unfortunately it is not support to build eabi toolchain,so I applied  a glibc 2.5+ nptl build forarm softfloat eabi patch  ( http://sources.redhat.com/ml/crossgcc/2006-12/msg00076.html)by Khem Raj.

$./arm-softfloat-eabi.sh

If the network is connected, the script will download andbuild toolchain using gcc 4.1.1 and glibc 2.5.

4.2 Othertoolchain 


I did not use the codesourcery toolchain, but they said itwill work for the building Android system.


5 Kernel 


To port the Android on a real hardware is started by Benno( http://benno.id.au), you can seesome useful information on his blog. On his blog some pre-compiledstatic binaries are linked. It is very helpful for debuggingAndroid system. You can also build static build busybox and stracebinaries, but it's better to get them and use.

You can get patch file including the differences between theAndroid kernel and the vanilla kernel with 2.6.23 version. It hasall differences between them. So you need to extract parts of them,and make your own patch for your system architecture.

For example, the Android kernel has it's own yaffs file systempatch. If you have your own yaffs or some other file systems likejffs2 on your architecture, then you need to remove the yaffs partsof the patch. The goldfish architecture which the Android kernelemulate an ARM architecture on qemu is not necessary part for yourarchitecture. It can be removed.

The Android kernel emulates  ARMv5  instructions.So  ARM926EJ-S ( ARMv5TEJ) will be good towork.

5.1 Patchkernel 


Benno played with a  NEO1973  deviceby openmoko. So he made patch files for it. Get the original patchfile from  http://benno.id.au/blog/2007/11/21/android-neo1973,I used android.diff. It has whole things about goldfish, qemu,yaffs, and Android specific parts.

You can edit and remove the patch file directly. After makingpatch including binder, android power, android logger, low memorykiller except goldfish and qemu specific parts, get vanilla 2.6.23version Linux kernel and patch it.

If you use a 2.6.24.1 version Linux kernel, some partregarding android power should be fixed accordingly or disabled towork.

5.2 .config 


  • Necessary
...
CONFIG_PANIC_TIMEOUT=0
CONFIG_AEABI=y
CONFIG_OABI_COMPAT=y
CONFIG_BINDER=y
CONFIG_LOW_MEMORY_KILLER=y
...

  • Optional
...
# CONFIG_ANDROID_GADGET is not set
# CONFIG_ANDROID_RAM_CONSOLE is not set
# CONFIG_ANDROID_POWER is not set
# CONFIG_ANDROID_LOGGER is not set
...

6 Root filesystem 


The root file system is composed of three parts such as aprimary ramdisk image on ram, a system image on nand dev0(/dev/block/mtdblock0), and a data image on nand dev1(/dev/block/mtdblock1). The mtd devices has a yaffs2 file systemand each of them has 64  MiB  capacityon the Android emulator.

The extracted system and data directories are copied to thereal existing NAND device and they are mounted with --bind optionto work on a real hardware.

6.1 Getramdisk image from emulator 


1. unpack ramdisk image from tools/lib/images/ramdisk.img
$ gzip -cd ramdisk.img > ramdisk
$ cpio -iv -F ramdisk

cpio will extract files and directories on current workingdirectory. 

2. the contents list of the ramdisk
data
dev
etc
etc/default.prop
etc/firmware
etc/firmware/brf6150.bin
etc/firmware/brf6300.bin
etc/hcid.conf
etc/hosts
etc/init.gprs-pppd
etc/init.rc
etc/init.ril
etc/init.testmenu
etc/ppp
etc/ppp/chap-secrets
etc/ppp/ip-down
etc/ppp/ip-up
etc/qemu-init.sh
etc/system.conf
etc/system.d
etc/system.d/bluez-hcid.conf
etc/usbd.conf
init
proc
sbin
sbin/recovery
sys
system
tmp
var
var/run

6.2 Getdata and system directory from emulator 


To get data and system directory you need a static compiledbusybox binary. The compiled binary can be obtainedfrom  http://benno.id.au/blog/2007/11/14/android-busybox  ,or make your own binary.

1. launch the Android emulator

2. push static compiled busybox into emulator
# adb push busybox .

3. launch the Android shell
# adb shell

4. make tarball with busybox
# chmod +x /busybox
# busybox tar -c /data.tar /data
# busybox tar -c /system.tar /system
# exit

5. extract tarball from the emulator
# adb pull /data.tar .
# adb pull /system.tar .

Extract command often failed. So you may repeat it again until ithas done successfully. 

6.3 Integratethe Android system with a existing ramdiskimage. 


The ramdisk for your architecture can make your work a biteasier. Copy the contents of the Android ramdisk to your ownramdisk except system and data directory. And make just mount pointfor system and data directory. The mount points will be used laterwith a bind option. The init binary of the Android ramdisk image isthe key binary to start the system and It read a configuration fileon /etc/init.rc.

Edit /etc/init.rc and comment out qemu part.

...
startup {
        ...
#       qemu-init {
#           exec /etc/qemu-init.sh
#       }
}
...

Make run.sh script. /dev/block/mtdblock5 is a mtd partition ona real NAND device, and it is mounted on /mnt. data and systemdirectories are already copied on mtdblock5. So the script belowjust shows bind mounting each directory on /. Fix your scriptaccording to your board configuration.

#!/bin/sh
mount -t yaffs /dev/block/mtdblock5 /mnt
mount --bind /mnt/data   /data
mount --bind /mnt/system /system

# data folder is owned by system user on emulator. Fix 777 to other.
chmod 777 /data
#chmod 777 /system

export PATH=/system/sbin:/system/bin:/sbin/usr/local/bin
export LD_LIBRARY_PATH=/system/lib

export ANDROID_BOOTLOGO=1
export ANDROID_ROOT=/system
export ANDROID_ASSETS=/system/app
export EXTERNAL_STORAGE=/sdcard
export ANDROID_DATA=/data
export DRM_CONTENT=/data/drm/content

/init &

An optional configuration for touchscreen-  TSLib.

...
export TSLIB_CONSOLEDEVICE=none
export TSLIB_FBDEVICE=/dev/fb0
export TSLIB_TSDEVICE=/dev/input/event1
export TSLIB_CALIBFILE=/etc/pointercal
export TSLIB_CONFFILE=/etc/ts.conf
export TSLIB_PLUGINDIR=/lib/ts

export LD_PRELOAD=/lib/libts.so:/lib/ts/pthres.so
...

6.4 Systemand Data directories 


The contents of the system and data directories are copied tomtdblock5 already. You should copy your own method. To use it, Ichose bind mounting on root directory. Bind mounting is a techniqueto mount an existing directory with a new mount point.

6.5 Run andDebug 


Now the kernel, ramdisk, and data directories - data andsystem - are ready. It's time to see the red cylon eye. After bootup your integrated system, run the run.sh on root directory.

# cd /
# . /android/run.sh
yaffs: dev is 32505861 name is "mtdblock5"
yaffs: passed flags ""
yaffs: Attempting MTD mount on 31.5, "mtdblock5"
yaffs: auto selecting yaffs2
# init: HOW ARE YOU GENTLEMEN
init: reading config file
init: device init
init: mtd partition -1,
init: mtd partition 0, "l1boot"
init: mtd partition 1, "u-boot"
init: mtd partition 2, "params"
init: mtd partition 3, "kernel"
init: mtd partition 4, "ramdisk"
init: mtd partition 5, "rootfs"
sh: can't access tty; job control turned off
# binder_open(c394bcc8 c3c731a0) (pid 1577) got c3e48000
binder_open(c394bcc8 c3cd8dc0) (pid 1616) got c319f000
binder_open(c394bcc8 c3cd8ac0) (pid 1673) got c3d10000
binder_open(c394bcc8 c3cd8940) (pid 1680) got c0e19000
binder_open(c394bcc8 c3cd88c0) (pid 1691) got c2fa0000
binder_open(c394bcc8 c3d174a0) (pid 1592) got c25b8000
binder_release(c394bcc8 c3cd88c0) (pid 1691) pd c2fa0000
#

  • Do not make eac device file on /dev. It is for the audio onqemu. If it exists, the start up sequence will wait forever tofinish writing some data to the sound device.
  • Use the Android init binary instead of manual startup. Themanual start up will require the android power patch. In that casethe start up sequence will access/sys/android_power/acquire_partial_wake_lock andwait. 

To debug the Android system, use static compiled strace binaryfrom  http://benno.id.au/blog/2007/11/18/android-runtime-strace  andrun the Android manually.

#!/bin/sh
# set environment variables above example
...
/system/bin/app_process -Xzygote /system/bin --zygote &
/system/bin/dbus-daemon --system &
/system/bin/runtime

The above example shows manual startup sequence, use strace onrun /system/bin/runtime binary.

./strace -ff -F -tt -s 200 -o /tmp/strace runtime

6.6 Screenshots 


  • Skipped 

7 ApplicationDevelopment 


The Android applications use Java syntax and xml layouts, butit is not a Java. Because they use their own virtual machine -dalvik - and compiler for dex file format. And use package namedapk such as Home.apk, Phone.apk, ApiDemos.apk and etc.

The apk file is a Zip archive and it has four parts.

  • AndroidManifest.xml
  • classes.dex
  • resources.arsc
  • res directory 

The Dex file format is explained on  http://www.retrodev.com/android/dexformat.html.And the contents of the files and directories are explained someday by Google. It is not explained currently. We can just guessabout it.

The Android SDK will create an *.apk file.

7.1 InstallEclipse IDE 



1. Eclipse IDE for Java developer (JDT and WST plugins areincluded) from  http://www.eclipse.org/downloads/


3. ADT (Android Development Tools) with a eclipse pluginincluding Apache Ant

7.2 Buildand Run Sample Applications 


1. Open sample projects and build

2. Run sample applications on emulator

7.3 Screenshots 


  • Android Platform on Nokia's N810 Product(arm1136jf-s)
    n810.kandroid200805.PNG

  • Android Platform on arm1136jf-S for another CE Product.
    fi.initial_display.jpg
  • invain님이 shot을 올려주셨군요.-- 이광우

8 Epilogue 


The Android system seems to be a new kind of a Linux baseddistribution for a mobile environment likeDebian,  RedHat SuSE, and etc. They just use theLinux kernel and a lot of different libraries in the open sourceworld. They offer a software based  OpenGL-ES library on a 3Dacceleration currently, but they are developing on a hardwareaccelerated baseband processor for it. The hardware acceleration isnecessary for fast UI rendering effects later.

The Android system on the sdk is not a completed one to porton a real hardware, because some device - for example, camera -related libraries and classes are not implemented yet and notopened for users. It seems to be under the development stage. So wewould better to wait the Google announces the whole portingkit.

Until then, we should look for the business model with theAndroid system. It requires a bit high cpu performance, so thecarrier vendors will require a cheap baseband processor (RF part)and a multimedia co-processor, because the baseband processorincluding multimedia features will be very expensive.

9 Links andReferences 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值