ARM Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

0239
1625
172602


android - recovery [Bootloader]

안드로이드 폰에서 Recovery 모드란 들어보신 적이 있나요? 
휴대폰 매장 직원도 종종 쓰는 표현인 것 같아요.
 
Recovery란  안드로이드 시스템에서 제공하고 있는 중요 기능 중에 하나랍니다.
이 기능에 대해서 좀 살펴볼까요?

보통 디바이스에 Recovery를 실행시키기 위한 파티션이 따로 있고, 부트로더(대부분 LK:Little Kernel)에서 특정 조건으로 Recovery를 실행시켜요.
그 조건은 팩토리 리셋을 시킨다던가, 다운로드 모드에 진입하는 조건들이죠.

그럼 Recovery 이미지를 생성하는 재료는 어떻게 구성될까요?
아래 MakeFile이 정답을 말해주고 있네요.
[android/build/core/Makefile]
$(INSTALLED_RECOVERYIMAGE_TARGET): $(MKBOOTFS) $(MKBOOTIMG) $(MINIGZIP) \
$(INSTALLED_RAMDISK_TARGET) \ // $(PRODUCT_OUT)/ramdisk.img //<<--[1]
$(INSTALLED_BOOTIMAGE_TARGET) \ // $(PRODUCT_OUT)/boot.img //<<--[2]
$(INTERNAL_RECOVERYIMAGE_FILES) \  // android/bootable/recovery //<<--[3]
$(recovery_initrc) $(recovery_sepolicy) $(recovery_kernel) \ //<<--$(call include-path-for, recovery)/etc/init.rc
$(INSTALLED_2NDBOOTLOADER_TARGET) \
$(recovery_build_props) $(recovery_resource_deps) \
$(recovery_fstab) \
$(RECOVERY_INSTALL_OTA_KEYS) \
$(INSTALLED_VENDOR_DEFAULT_PROP_TARGET) \
$(BOARD_RECOVERY_KERNEL_MODULES) \
$(DEPMOD)
$(call build-recoveryimage-target, $@)

크게 아래 이미지로 구성된다고 보면 되죠.
1> 램디스크 이미지(루프 파일 시스템)
2> 커널 이미지
3> android/bootable/recovery 에 있는 코드들
4> android/bootable/recovery/etc/init.rc

그럼 어떤 놈이 recovery를 실행시킬까요? android/bootable/recovery/etc/init.rc 코드를 열어서 보면 상세 정보를 알 수 있어요.
아래 코드를 보면 init process와 파일 시스템을 마운트하고 recovery를 서비스 형태로 실행시키죠.
4on early-init
5    # Set the security context of /postinstall if present.
6    restorecon /postinstall
7
8    start ueventd
9
10on init
11    export ANDROID_ROOT /system
12    export ANDROID_DATA /data
13    export EXTERNAL_STORAGE /sdcard
14
15    symlink /system/etc /etc

112service recovery /sbin/recovery
113    seclabel u:r:recovery:s0

보통 안드로이드 모바일에서 Recovery 모드는 3가지 시나리오 지원해요.
1> 세팅 메뉴에서 팩토리 리셋을 선택하였을 때 리부팅 된 다음에 진입
2> 전원이 꺼진 상태에서 키맵(제조사 마다 다르죠)으로 팩토리 리셋에 진입
3> GOTA upgrade

가끔 Recovery 모드에서 팩토리 리셋이 제대로 안된다 등 화면이 깨진다 등등 이슈가 있다고 연락이 와요.
이럴 때 보통 아래 폴더에 있는 로그를 열고 디버깅을 할 수 있죠.
/cache/recovery/last_log, last_log.2

아래 recovery 모드 실행 시 아래 로그를 볼 수 있어요.
...
locale is [en_US]
stage is []
failed to read font: res=-1
framebuffer: fd 4 (800 x 1280)
       installing_text: en (324 x 38 @ 411)
          erasing_text: en (109 x 38 @ 312)
       no_command_text: en (172 x 38 @ 312)
            error_text: en (64 x 38 @ 312)
I:Max_satage-1: 17299
I:SetBackGround - : 50
I:Max_satage-2: 17299
Command: "/sbin/recovery"

그러면 Recovery mode에서 주로 쓰이는 Factoy Reset Feature에 대해서 좀 살펴볼까요?
팩토리 리셋은 크게 세팅 메뉴로 동작시킬 수 있고, 각 제조사가 정의한 모드를 통해 동작시킬 수 있습니다.

세팅 메뉴에서 팩토리 리셋을 선택할 시 동작 순서는 아래와 같아요.
1> /cache/recovery/command 파일을 만들고 여기에 argument 값 --wipe_data 를 씀
2> 리부팅 됨
3> LK(Little Kernel)에서 misc 파티션에 아래와 같은 값을 써줌
      bootMessage.command="boot-recovery"
      bootMessage.status[0] = (char)0;
      bootMessage.recovery="recovery\n--wipe_data\n"
4> 리커버리가 실행(android/bootable/recovery)가 되고 /cache/recovery/command 파일을 찾아서 --wipe_data 값이 있는 지 확인
5> /cache, /data 파티션을 날려 버림

리커버리 모드 코드에서 파악할 중요한 포인트는 어떻게 argument을 읽어오며 이에 따라 어떤 동작을 하는 지,
각 파티션은 어떻게 날려버리는 지가 중요하죠.

대표적인 동작 별 argument는 아래와 같습니다.
팩토리 리셋: --wipe_out, GOTA: --update_package 

이제 코드 좀 볼까요? 각 bootloader나 커널의 실행 시작 포인트는 reset vector인데 
데몬 형태의 프로세스들은 main으로 시작하죠.

[1]: 나 리커버리, 시작된다!
[2]: boot argument를 읽어오자.
[3]: boot argument값에 따라 recovery 동작에 대한 정의
[4]: recovery mode 동작 이후 디바이스를 shutdown 시키는 argument
[5]: 언어 모드를 설정
[6][7]: 화면에 뿌릴 UI와 text를 초기화
[8]: argument를 로그에 찍어 줌
[9]: property를 로그로 출력
[10]: update_package이 NULL이면 실행 안함
[11]: 밧데리 레벨이 낮으면 튕겨낼 코드, GOTA package 업데이트 중 밧데리 방전되면 참 골치 아프지.
[12]: GOTA package update
[13]: 팩토리 리셋
[14]: 패키지 업데이트 도중 Fail 시 reboot을 칠 예외 코드
[15]: 팩토리 리셋
[16]: 디바이스 셧다운
[17]: 부트로더로 다시 리부팅
[18]: 디폴트로 리부팅 시킴 
int main(int argc, char **argv) {
    // We don't have logcat yet under recovery; so we'll print error on screen and
    // log to stdout (which is redirected to recovery.log) as we used to do.
    android::base::InitLogging(argv, &UiLogger);
// ... skip ...
    printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));  //<<--[1]

    load_volume_table();
    has_cache = volume_for_path(CACHE_ROOT) != nullptr;
//.. skip ..
    std::vector<std::string> args = get_args(argc, argv); //<<--[2]
    while ((arg = getopt_long(args_to_parse.size(), args_to_parse.data(), "", OPTIONS,
                              &option_index)) != -1) {
        switch (arg) {  //<<--[3]
        case 'n': android::base::ParseInt(optarg, &retry_count, 0); break;
        case 'u': update_package = optarg; break;
        case 'w': should_wipe_data = true; break;
        case 'c': should_wipe_cache = true; break;
        case 't': show_text = true; break;
        case 's': sideload = true; break;
        case 'a': sideload = true; sideload_auto_reboot = true; break;
        case 'x': just_exit = true; break;
        case 'l': locale = optarg; break;
        case 'p': shutdown_after = true; break;  //<<--[4]
        case 'r': reason = optarg; break;
        case 'e': security_update = true; break;
        case 'f': fota_mode = true; break;
    }

    if (locale.empty()) { //<<--[5], start
        if (has_cache) {
            locale = load_locale_from_cache();
        }

        if (locale.empty()) {
            locale = DEFAULT_LOCALE;
        }
    } //<<--[5], end

    printf("locale is [%s]\n", locale.c_str());
    printf("stage is [%s]\n", stage.c_str());
    printf("reason is [%s]\n", reason);

    Device* device = make_device();
    if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
        printf("Quiescent recovery mode.\n");
        ui = new StubRecoveryUI();
    } else {
        ui = device->GetUI(); 

        if (!ui->Init(locale)) {
            printf("Failed to initialize UI, use stub UI instead.\n");
            ui = new StubRecoveryUI();
        }
    }

    // Set background string to "installing security update" for security update,
    // otherwise set it to "installing system update".
    ui->SetSystemUpdateText(security_update); //<<--[6]
// ... skip ...
    ui->SetBackground(RecoveryUI::NONE); //<<--[7]
    if (show_text) ui->ShowText(true);
// ... skip ...

    sehandle = selinux_android_file_context_handle();
    selinux_android_set_sehandle(sehandle);
    if (!sehandle) {
        ui->Print("Warning: No file_contexts\n");
    }

    device->StartRecovery();

    printf("Command:");
    for (const auto& arg : args) {
        printf(" \"%s\"", arg.c_str()); //<<--[8]
    }
    printf("\n\n");

    property_list(print_property, NULL); //<<--[9]
    printf("\n");

// ... skip ...

    if (update_package != NULL) {  //<<--[10]
        // It's not entirely true that we will modify the flash. But we want
        // to log the update attempt since update_package is non-NULL.
        modified_flash = true;

        if (!is_battery_ok()) { //<<--[11]
            ui->Print("battery capacity is not enough for installing package, needed is %d%%\n",
                      BATTERY_OK_PERCENTAGE);
            // Log the error code to last_install when installation skips due to
            // low battery.
            log_failure_code(kLowBattery, update_package);
            status = INSTALL_SKIPPED;
        } else if (bootreason_in_blacklist()) {
            // Skip update-on-reboot when bootreason is kernel_panic or similar
            ui->Print("bootreason is in the blacklist; skip OTA installation\n");
            log_failure_code(kBootreasonInBlacklist, update_package);
            status = INSTALL_SKIPPED;
        } else {
            status = install_package(update_package, &should_wipe_cache, //<<--[12]
                                     TEMPORARY_INSTALL_FILE, true, retry_count);
            if (status == INSTALL_SUCCESS && should_wipe_cache) {
                wipe_cache(false, device); //<<--[13]
            }
            if (status != INSTALL_SUCCESS) {
                ui->Print("Installation aborted.\n");
                // When I/O error happens, reboot and retry installation EIO_RETRY_COUNT
                // times before we abandon this OTA update.
                if (status == INSTALL_RETRY && retry_count < EIO_RETRY_COUNT) {
                    copy_logs();
                    set_retry_bootloader_message(retry_count, args);
                    // Print retry count on screen.
                    ui->Print("Retry attempt %d\n", retry_count);

                    // Reboot and retry the update
                    if (!reboot("reboot,recovery")) { //<<--[14]
                        ui->Print("Reboot failed\n");
                    } else {
                        while (true) {
                            pause();
                        }
                    }
                }
// ... skip ...
    } else if (should_wipe_data) {
        if (!wipe_data(device)) { //<<--[15]
            status = INSTALL_ERROR;
        }

    // Save logs and clean up before rebooting or shutting down.
    finish_recovery();

    switch (after) {
        case Device::SHUTDOWN:  //<<--[16]
            ui->Print("Shutting down...\n");
            android::base::SetProperty(ANDROID_RB_PROPERTY, "shutdown,");
            break;

        case Device::REBOOT_BOOTLOADER: //<<--[17]
            ui->Print("Rebooting to bootloader...\n");
            android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,bootloader");
            break;

        default:
            ui->Print("Rebooting...\n"); //<<--[18]
            reboot("reboot,");

Recovery를 디버깅할 때 가장 중요한 함수는 get_args() 이거든요.
좀 더 상세히 살펴볼까요?
[1]: misc 파티션에서 boot message를 읽어와요.
[2]: 부트 커맨드에 대한 로그를 뿌려요.
[3]: \n 단위로 스트링을 짤라줘요.
  argument가 “recovery \n --wipe_data” 인 경우,
  이렇게 되는 거죠. tokens[0] = recovery, tokens[1] = --wipd_data
[4]: COMMAND_FILE를 읽어서 처리를 하는 루틴인데, 세팅 메뉴로 팩토리 리셋을 선택할 경우 동작하죠.   
static std::vector<std::string> get_args(const int argc, char** const argv) {
  CHECK_GT(argc, 0);

  bootloader_message boot = {};
  std::string err;
  if (!read_bootloader_message(&boot, &err)) { //<<--[1]
    LOG(ERROR) << err;
    // If fails, leave a zeroed bootloader_message.
    boot = {};
  }
  stage = std::string(boot.stage);

  if (boot.command[0] != 0) {
    std::string boot_command = std::string(boot.command, sizeof(boot.command));
    LOG(INFO) << "Boot command: " << boot_command; //<<--[2]
  }

  if (boot.status[0] != 0) {
    std::string boot_status = std::string(boot.status, sizeof(boot.status));
    LOG(INFO) << "Boot status: " << boot_status;
  }

  std::vector<std::string> args(argv, argv + argc);

  // --- if arguments weren't supplied, look in the bootloader control block
  if (args.size() == 1) {
    boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
    std::string boot_recovery(boot.recovery);
    std::vector<std::string> tokens = android::base::Split(boot_recovery, "\n");
    if (!tokens.empty() && tokens[0] == "recovery") { 
      for (auto it = tokens.begin() + 1; it != tokens.end(); it++) {  //<<--[3]
        // Skip empty and '\0'-filled tokens.
        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
      }
      LOG(INFO) << "Got " << args.size() << " arguments from boot message";
    } else if (boot.recovery[0] != 0) {
      LOG(ERROR) << "Bad boot message: \"" << boot_recovery << "\"";
    }
  }

  // --- if that doesn't work, try the command file (if we have /cache).
  if (args.size() == 1 && has_cache) {
    std::string content;
    if (ensure_path_mounted(COMMAND_FILE) == 0 &&  //<<--[4]
        android::base::ReadFileToString(COMMAND_FILE, &content)) {
      std::vector<std::string> tokens = android::base::Split(content, "\n");
      // All the arguments in COMMAND_FILE are needed (unlike the BCB message,
      // COMMAND_FILE doesn't use filename as the first argument).
      for (auto it = tokens.begin(); it != tokens.end(); it++) {
        // Skip empty and '\0'-filled tokens.
        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
      }
      LOG(INFO) << "Got " << args.size() << " arguments from " << COMMAND_FILE;
    }
  }

마지막으로 misc 파티션에 명시된 recovery argument을 읽어와 boot.command 변수에 붙혀주는 
함수는 write_bootloader_message() 이네요.
예를 들어 이렇게? boot.recovery = "recovery"+"--wipe_data
bool write_bootloader_message(const std::vector<std::string>& options, std::string* err) {
  bootloader_message boot = {};
  strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
  strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
  for (const auto& s : options) {
    strlcat(boot.recovery, s.c_str(), sizeof(boot.recovery)); //<<--
    if (s.back() != '\n') {
      strlcat(boot.recovery, "\n", sizeof(boot.recovery));
    }
  }
  return write_bootloader_message(boot, err);
}  


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2




덧글

댓글 입력 영역