Android 아키텍쳐

Android 아키텍쳐

2024년 3월 26일
android, os

아키텍쳐 프리뷰 #

f8fd7095-f68f-4c62-8718-8aa23e73e244

  • Linux Kernel : 하드웨어의 추상화, 네트워크 스택, 메모리관리, 프로세스관리 등 하드웨어와 직접적인 핵심 시스템 서비스를 담당한다. 리눅스 커널을 그대로 사용하진 않고, 모바일 기기에 최적화되도록 수정해서 사용한다.
    • 기존 리눅스 기능 : VFS, SELinux, IPC 등
    • 리눅스 커널 함수 : read, write, open, close, ioctl, syscall
    • 추가된 기능들 : LowMemoryKiller, Binder IPC, wakelock(절전중 깨움 제어), AVB(안드로이드 검증 부팅) 등
  • HAL : 벤더사의 하드웨어와 안드로이드 사이의 추상화를 담당한다. 프레임워크에서 사용하는 드라이버 코드를 구글에서 표준화된 인터페이스를 사용하도록 강제하고, C/C++ 함수로 API를 사용할 수 있게 제공한것이다.
    • 카메라 : open_camera, close_camera, set_preview_window 등의 C/C++ 함수
  • NativeLibrary : JNI를 통해 Java 코드에서 호출할 수 있는 C/C++ 함수를 만들 수 있고, 반대로 Java 메서드를 호출할수도 있다. 그림은 무조건 HAL을 통하게 되어있지만, 벤더 장치에 접근하는걸 제외하면 직접 커널 함수를 호출한다. ex) syscall
  • Android Runtime : AOT컴파일을 사용해서 설치시점에 자바 바이트코드(컴파일된 앱)를 기계어로 컴파일한다.
  • Java Framework : JAVA API를 제공한다. 벤더 하드웨어를 조작하는 Java코드는 ART를 통해 HAL의 함수를 호출하게된다.
    • 센서를 예로들면 SensorManager 클래스는 JNI를 통해 HAL이 제공하는 sensors.h에 정의된 native 함수를 사용한다.
  • System Apps : 시스템에 기본적으로 설치되어 있는 카메라, 캘린더 등의 앱을 말한다. 일반 사용자 앱보다 높은 권한을 갖게된다.
    pm list packages -s 로 설치된 시스템 앱들을 확인할 수 있다. 3ef8ac49-c97a-4f4b-9d20-8036bb5a6739

HAL (Hardware Abstract Layer) #

AOSP 개발 순서 #

f0e0b7e4-a594-4d26-995b-ffed4609d888

  • Google : AOSP를 관리한다.
  • SoC 벤더 : 퀄컴(스냅드래곤), 미디어텍, 삼성(엑시노스) 핵심 하드웨어를 제작하고 공급한다.
  • OEM : 삼성(갤럭시), 구글(픽셀), 샤오미(레드미), OPPO(리얼미) 등 하드웨어를 조립하고 패키징해서 판매한다.
  • 통신사 : 네트워크의 호환성 인증과 커스터마이징을 담당한다.
  • CTS : 기기에 설치된 OS가 안드로이드 앱을 일관성있게 실행시킬 수 있는지 테스트하는것. 이미 구현된 앱이 호출하는 시스템 함수들에 대해 신규 OS에서 변한건 없는지 체크하는 것이다. (=기기에 설치된 OS가 플랫폼 버전에 맞게 API를 구현했는지 체크하는것)
  • VTS : SoC 벤더가 작성한 HIDL/AIDL구현체(.cpp, .java)가 구글이 제공한 HAL 인터페이스(.hal/.aidl로 인터페이스만 제공)를 잘 구현했는지 테스트

Treble 이전 개발 순서 #

  1. Google 안드로이드 팀은 최신 릴리즈의 오픈소스를 공개한다. 기본 HAL 인터페이스를 제공
  2. 안드로이드 기기에 사용되는 칩을 만드는 SoC 업체는 하드웨어에 맞게 AOSP를 수정한다. 커널모듈, 드라이버, HAL의 구현체를 추가하는 작업을 한다.
  3. 안드로이드 기기를 만드는 OEM 업체는 SoC 업체의 운영체제에 기기 특화 기능 추가 후 CTS 테스트.
    ex) 삼성의 OneUI
  4. OEM 업체가 통신사에 맞춰서 네트워크관련 코드를 수정하고 CTS가 통과되면 기기에 설치한다.
  5. 사용자에게 공개되고 OS 업데이트 시 위 과정을 반복한다.

안드로이드는 하드웨어가 정말 다양하고, 운영체제도 다양한데 개발자 입장에서는 하나의 앱만 구현해도 모든 기기에서 실행될 수 있는 엄청난 장점이 있고 그것은 안드로이드 프레임워크(SoC, OEM 수정 버전)가 제공하는 API가 CTS를 통과했기 때문에 가능한 일이였다.

하지만 하드웨어와 AOSP 사이에는 그런 테스트가 없어서 회사마다 HAL이나 AOSP 프레임워크를 자신의 회사 코드에 맞게 다시 수정하며 사용했고, 새 운영체제가 발표될 때마다 같은 작업을 반복하여 6~12개월의 운영체제 업데이트 지연이 발생했다.


Treble 프로젝트 #

HAL을 재설계해서 벤더 구현과 OS 프레임워크를 명확하게 분리한 프로젝트이다. HAL 인터페이스의 표준화를 목표로 HIDL/AIDL로 표준화하고, VTS 테스트를 도입했다.

벤더는 하드웨어에 맞는 HIDL/AIDL 구현체를 작성한 뒤 AOSP의 HAL 인터페이스와의 호환성을 보장하는 VTS 테스트로 검증하도록 하여 하드웨어가 운영체제에 호환이 되도록 만든것이 Treble 프로젝트(AOS 8)이다.

마치 Vulkan에서 API 인터페이스만 구현해두고 하드웨어 제조사가 드라이버를 개발할때 API에 맞춰서 개발하는것과 같다.


레거시 HAL #

HAL의 목표는 프레임워크에서 하드웨어의 제조사마다 다른 초기화, 사용 등의 함수 인터페이스를 신경쓰지 않도록 표준화한 것이다.
Android 8 이전의 레거시 HAL에서는 .h 파일로 구글이 인터페이스를 제공해주면 벤더사가 인터페이스의 내부코드를 구현하고 하드웨어 드라이버와 통신하도록 만들었다.

제공된 헤더에 맞는 c/c++ 코드를 작성한 후 빌드하면 sensors.qcom.so 과 같은 이름으로 빌드되며 프레임워크(유저영역)에서는 이 라이브러리를 dlopen으로 열어서 제공했던 헤더에 맞게 드라이버를 호출하게 된다.

이렇게 구현하면 AOSP에서는 모든 하드웨어를 대상으로 같은 인터페이스를 호출하며 수많은 벤더에 맞출 필요도 없으며 벤더의 드라이버 업데이트도 AOSP업데이터 없이 별도로 적용할 수 있게된다.

하지만 HAL의 경계에 대한 강제성이 없었기 때문에 벤더들은 커널+HAL+프레임워크 패치까지 적용해서 하드웨어를 지원했었다.


바인더 HAL #

treble 프로젝트(AOSP 8) 이후 AOSP에서는 하드웨어를 모듈식으로 제공할 수 있도록 추상화레이어를 강제시켰다.

1. HAL 인터페이스 #

구글이 기본적으로 제공하기도 하고, 벤더 전용 HAL인 경우 벤더에서 정의할수도 있다.

1// hardware/interfaces/vibrator/1.0/IVibrator.hal
2package android.hardware.vibrator@1.0;
3
4interface IVibrator {
5    oneway void on();     // oneway = 비동기
6    oneway void off();
7};

hidl-gen 을 실행시켜 빌드하면 위의 .hal 인터페이스 정의를 읽어 클라이언트 프록시/서버 스텁/패스스루 래퍼/빌드 스켈레톤 등을 생성


2. HAL 서비스 구현 #

HAL 인터페이스를 상속해서 메서드를 구현하고, main에서 서비스를 등록해두면 나중에 프레임워크에서 이 서비스를 찾아서 사용할 수 있게된다.
이 코드는 빌드해서 /vendor 파티션에 넣어둔다.

 1using ::android::hardware::vibrator::V1_0::IVibrator;
 2struct Vibrator : public IVibrator {
 3  ::android::hardware::Return<void> on() override { /* 드라이버 제어 */ return {}; }
 4  ::android::hardware::Return<void> off() override { /* ... */ return {}; }
 5};
 6int main() {
 7  android::sp<Vibrator> svc = new Vibrator();
 8  svc->registerAsService("default");  // hwservicemanager 등록
 9  android::hardware::joinRpcThreadpool();
10}

3. 서비스 실행 #

서비스의 main 함수(hwservicemanager에 등록하는 역할)는 부팅 시 init 프로세스가 rc 파일을 읽어 HAL에 속한 서비스들을 시작한다.

1// /vendor/etc/init/android.hardware.vibrator@1.0-service.rc
2service vendor.vibrator-1-0 /vendor/bin/hw/android.hardware.vibrator@1.0-service
3interface android.hardware.vibrator@1.0::IVibrator default
4class hal
 1// /vendor/etc/vintf/manifest.xml
 2<hal format="hidl">
 3  <name>android.hardware.vibrator</name>
 4  <transport>hwbinder</transport>
 5  <version>1.0</version>
 6  <interface>
 7    <name>IVibrator</name>
 8    <instance>default</instance>
 9  </interface>
10</hal>

4. 서비스를 통해 HAL 함수 호출 #

안드로이드 프레임워크에서는 필요할때 Binder IPC(안드로이드 표준 RPC)를 통해 HAL 서비스(프로세스)를 가져오고 함수를 호출한다.

1using ::android::hardware::vibrator::V1_0::IVibrator;
2
3sp<IVibrator> vib = IVibrator::getService("default");  // 발견
4if (vib) vib->on();  // Binder IPC 호출

AIDL은 HIDL보다 먼저 구현되어 안드로이드에서 프로세스간 통신을 지원하기 위해 만들어진 인터페이스 정의 언어였다.
Android 11 이후부터 HAL인터페이스로 AIDL을 사용할 수 있도록 통합되었고 HIDL의 백엔드는 HAL 전용으로 구현된 libhwbinder 에 있지만, HAL AIDL은 기존 AIDL도 사용하던 libbinder_ndk 을 그대로 사용하기 때문에 플랫폼에 안정적으로 사용할 수 있다.


커널 #

부트롬의 부트로더가 여러단계를 거쳐서 커널을 메모리에 로드하고 실행시킨다.

일반적으로 리눅스 LTS 커널에 안드로이드 전용 패치를 얹은 커널을 ACK(Android Common Kernel) 커널이라고 부른다.

여기에 벤더사에서 기기에 맞게 커널을 추가 수정하게 되는데, 이 방식은 중요한 보안패치가 발생해도 바로 적용하기 힘들어서 android 11부터 커널을 하드웨어 독립적인 범용 이미지 구조로 표준화(GKI)했다.

non-GKI (pixel 2xl 기준) #

android-msm-wahoo-4.4-android10-qpr3 브랜치를 사용한다. msm은 android kernel 소스코드에서 퀄컴 SoC 전용코드가 포함된 kernel 소스트리임을 의미한다.

uname -a 명령으로 현재 기기에 설치된 커널버전의 커밋번호를 확인할 수 있다. 설치할 이미지의 boot.img 에서 strings를 검색해봐도 확인할 수 있다. 5acbc134-f173-4a23-8270-8a2c79b35c52

 1$ mkdir android-kernel && cd android-kernel
 2$ repo init -u https://android.googlesource.com/kernel/manifest -b android-msm-wahoo-4.4-android10-qpr3
 3$ repo sync
 4
 5# 1) 환경 로드
 6# _setup_env.sh에서 TOOLCHAIN PATH, OUT_DIR, DEFCONFIG, TOOL_ARGS 등 환경변수를 잡아줌
 7$ export BUILD_CONFIG=build.config
 8$ source build/_setup_env.sh
 9
10# 2) 디펜던시 생성 + defconfig 적용
11$ make -C "${KERNEL_DIR}" O="${OUT_DIR}" ${TOOL_ARGS} "${DEFCONFIG}"
12
13# 3) 커널 컴파일
14$ make -C "${KERNEL_DIR}" O="${OUT_DIR}" ${TOOL_ARGS} -j"$(nproc)"

빌드 산출물 #

out/android-msm-wahoo-4.4/private/msm-google/arch/arm64/boot/

44a4e5cc-0d41-480f-93cd-7dcc65f99552

  • vmlinux : ELF포맷으로 링크된 디버깅용 이미지 (심볼포함)
  • System.map : 커널 심볼과 주소 매핑표이며 크래시 분석(KASLR 오프셋 해석)에 사용된다.
  • Image : ARM64 커널의 부팅용 바이너리이미지. 헤더가 없는 raw 이미지이며, 커널 본체만 포함된다. (일부 벤더의 수정코드 포함)
  • Image.lz4 : Image를 lz4로 압축한 것
  • Image.lz4-dtb : 일부 부트로더는 Image.lz4 뒤에 베이스 DTB를 그대로 이어붙인(concat) 파일을 요구해서 이런 파일로 빌드됨. mkbootimg 로 램디스크와 패키징되어 boot.img 파일이 만들어지고 boot 파티션에 램디스크와 함께 저장된다.
  • dtbo.img : DTB Overlay들의 묶음. 기본 SoC DTB 위에 기기마다 다를 수 있는 보드/SKU/리비전별 차이를 오버레이로 적용할때 쓰며 dtbo 파티션에 플래시된다. 부트로더가 기기를 식별해서 해당되는 overlay를 골라 적용한다.
  • DTB(Device Tree Blob) : 보드 하드웨어를 기술한 내용이 담긴 바이너리이며 커널은 부팅시 DTB를 읽어서 드라이버를 보드에 맞게 초기화한다.

커널 빌드에서는 위의 파일만 나오고, 플랫폼을 빌드할때 kernel과 ramdisk가 패키징되며 boot.img 파일이 빌드된다.


GKI (pixel 6a 기준) #

Android 12부터 커널버전 5.10 이상으로 제공되는 기기는 GKI 커널이 의무적으로 사용된다.

이전에는 커널이 벤더의 하드웨어와도 의존적인 코드가 있어서 벤더가 한번 더 수정해야됐기 때문에 보안패치 등 업데이트가 늦는 문제가 생겼는데, GKI 프로젝트로 하드웨어에 의존적이지 않은 부분만 AOSP의 커널로 만들고 의존적인 부분은 벤더모듈(vendor_boot + vendor_dlkm)로 분리하여 따로 일반커널과 벤더커널 업데이트가 가능하도록 만들었다.

벤더 커널모듈(.ko)은 무조건 KMI에서 허용된 안정 심볼만 사용하도록 강제되어 있고, 그걸 커널에서 호출해주는 방식이기 때문에 안정적으로 분리할 수 있었다.

  • 테스트 커널
    • pixel 6a AOS13
    • kernel manifest: android-gs-bluejay-5.10-android13-qpr3
    • AOS: bluejay-tq3a.230901.001-factory-141a3d5b
    • build OS: WSL ubuntu 22.04 (24.04에서는 컴파일 시 심볼에러 발생)
1$ mkdir android-kernel && cd android-kernel
2$ repo init -u https://android.googlesource.com/kernel/manifest -b android-gs-bluejay-5.10-android13-qpr3
3$ repo sync -c --no-tags
4$ ./build_bluejay.sh     # 6a 기준. build_{DEVICE}.sh

빌드 산출물 #

GKI 커널과 Pixel 드라이버 모듈까지 포함된다


커널 빌드 이미지 사용 #

플래싱 #

TODO: 기존 이미지에 끼워넣기

플래싱하지 않고 부팅 #

적용하고나면 fastboot에서 나오게 되고, 실제로 플래싱하는게 아니기때문에 재부팅하면 기존 커널로 돌아온다.

1$ adb reboot bootloader
2$ fastboot boot Image.lz4-dtb

빌드옵션 수정 #

KASAN 옵션을 추가하면 디버깅을 위해 로컬 스택 프레임을 더 크게 사용해야하기 때문에 빌드시 스택프레임크기 경고가 에러로 승격되어 실패한다. frame-larger-than 를 에러로 승격되지 않도록 플래그를 설정하면 된다.

빌드옵션 링크에 포함된 모든 옵션이 지정되진 않았지만 빌드에 성공했고, 권장사항에 맞게 전부 지정해도 괜찮을것 같다.

 1$ export BUILD_CONFIG=build.config
 2$ vi build.config
 3...
 4# KASAN 빌드옵션 추가
 5POST_DEFCONFIG_CMDS="check_defconfig && update_debug_config"
 6function update_debug_config() {
 7    ${KERNEL_DIR}/scripts/config --file ${OUT_DIR}/.config \
 8         -d CONFIG_KERNEL_LZ4 \
 9         -e CONFIG_KASAN \
10         -e CONFIG_KASAN_INLINE \
11         -e CONFIG_KCOV \
12         -e CONFIG_SLUB \
13         -e CONFIG_SLUB_DEBUG \
14         --set-val FRAME_WARN 0
15    (cd ${OUT_DIR} && \
16     make O=${OUT_DIR} $archsubarch CC=${CC} CROSS_COMPILE=${CROSS_COMPILE} olddefconfig)
17}
18
19$ source build/_setup_env.sh
20$ make -C "${KERNEL_DIR}" O="${OUT_DIR}" ${TOOL_ARGS} "${DEFCONFIG}"
21$ make -C "${KERNEL_DIR}" O="${OUT_DIR}" ${TOOL_ARGS} KCFLAGS="-Wno-error=frame-larger-than=" -j"$(nproc)"
22
23# 적용됐는지 확인
24$ adb shell zcat /proc/config.gz | grep 'CONFIG_KASAN'

부트로더 (+TEE/TA) #

보통 부트로더에 TEE와 TA까지 포함되어있다. Pixel 2XL 에서는 공장초기화 이미지에 bootloader-*.img 이름으로 만들어져 있고, bootloader 파티션에 플래시된다. 삼성의 경우에도 sboot.img에 부트로더가 포함되어있다.

0e8c37e3-f1eb-4be6-8077-37aa42f8cc63

보통 부트로더에는 TEE, TA가 포함되어 있는데, tzar.img, tzsw.img 등으로 나뉘어 있을 수 있다.


파티션 #

파티션은 저장장치(eMMC, UFS 등)를 논리적으로 나누는 단위이고, 시스템 소프트웨어나 사용자 데이터, 하드웨어 코드를 분리해서 관리한다.

안드로이드 버전에 따라서(특히 8~11 이 사이) 구조가 많이 변경되기 때문에 크게 변한 부분만 살펴보자.

ramdisk #

초기 하드웨어 초기화 및 파티션 마운트를 처리하기 위해 커널이 임시로 ramdisk를 메모리(RAM)에 로드 후 임시 루트 파일시스템으로 사용한다.


9 이전 파티션 #

커널에서 램디스크 자체를 RAM에서 내리지 않고 그대로 루트 파일시스템으로 사용하며 따로 /system 파티션이나 /vendor 파티션을 디스크 마운트한다.

96ab233e-9d36-44aa-9392-912cfb982cfb

  • bootloader.img : 부트로더가 포함됨
  • radio.img : 통신관련 소프트웨어 포함
  • boot.img : 커널과 ramdisk가 포함되고, ramdisk에는 루트 파일시스템까지 포함되어있었다. 벤더별로 커널 호환성을 위해서나 일부 커널 드라이버가 빌트인으로 구현되어야 하는경우 수정사항도 포함됐다.
  • system.img : /system 파티션이 포함되어
  • vendor.img : 기기제조사 특화 바이너리와 드라이버(커널모듈 .ko)를 포함한 벤더 이미지이다.
  • vbmeta.img : android 8 이상에서 AVB(Android Verified Boot)에 사용되는 메타데이터
  • tz.img : TrustZone 펌웨어 이미지.
  • keymaster.img : Keystore 하드웨어 지원 키 관리자 펌웨어

9 이후 파티션 #

system-as-root 구성으로 인해 루트 파일시스템은 ramdisk.img 에 포함되지 않고, system.img에 포함된다. 커널이 램디스크를 임시로 사용하다가 system.img를 루트파일시스템으로 마운트하게된다.


GKI 이후 파티션 #

GKI로 커널이 분리되고 나서는 부트로더가 boot(커널)을 로드하고 init_boot(제네릭램디스크)와 vendor_boot(벤더램디스크) 가 합쳐져 초기화가 진행된 후 유저영역까지 부팅되면 system_dlkm과 vendor_dlkm 파티션의 모듈들이 로드되는 방식이라 벤더와의 의존성을 끊고 별도 업데이트가 가능해졌다.

  • boot.img : GKI 커널이미지 (+ 제네릭 램 디스크)
  • init_boot.img : AOS13 출시 기기부터 제네릭 램디스크를 분리해서 담아뒀다. 이 파일이 있다면 boot.img에는 GKI 커널만 담겨있게된다.
  • vendor_boot.img : 밴더 램디스크(+DTB)가 들어있는 이미지. 벤더의 하드웨어별 초기화코드/드라이버가 포함된 램디스크는 여기에 저장된다. 보통 스크립트/설정/테이블이 포함되어 부팅 시 하드웨어별 초기화를 담당하는데 커널모듈(.ko)도 포함되기도 한다.
  • system_dlkm.img : 이것도 커널모듈인거같은데?? TODO 왜나는없냐 있는녀석맞음? 그냥 파티션인가? 일반램디스크에포함되는? vendor_dlkm도 서브파티션이라햇던거같은데
  • vendor_dlkm.img : 동적 로더블 벤더 커널 모듈이 포함된 이미지이다. 벤더 커널 모듈을 시스템과 분리한다.
comments powered by Disqus