1. 배경
내 앱이 왜 그렇게 멈췄나요? 누가 코드를 오염시켰나요?
어느 날 갑자기 디버그 패키지가 매우 느리게 실행되는 것을 발견했습니다. 다음과 같은 간단한 테스트를 마친 후 Android 14의 디버그 패키지에 문제가 있음을 발견했습니다.
2. 문제해결 기록
일상적인 조사 수단
문제 해결을 위해 systrace 및 내부 디버그 패키지 추적 도구 dutrace를 사용했습니다.
결론: CPU가 유휴 상태이고 메인 스레드가 명확하게 차단되지 않은 것으로 보입니다. 순수 메서드 실행에는 시간이 많이 걸리는 것 같습니다.
의심스러운 점 발견
문제 해결의 첫 번째 단계에서는 큰 이득이 없었지만, 문제 해결을 위해 dutrace 도구를 사용했을 때 이상 현상을 발견했습니다. 다음은 dutrace의 구현 원리에 대한 간략한 소개입니다.
Dutrace는 인라인 후크를 사용하여 artmethod 실행 전후에 atrace 포인트를 추가한 다음 perfetto ui 도구를 통해 표시합니다. 다음과 같은 장점이 있습니다.
1. 기능 실행 프로세스 및 시간 소모적인 기능에 대한 오프라인 분석을 지원합니다.
2. 분석 함수 호출 프로세스에서:
a. 전체 프로세스(프레임워크 기능 포함)의 함수 호출을 볼 수 있습니다.
b. 쓸모없는 추적을 효과적으로 필터링하기 위해 모니터링되는 기능 및 스레드를 지정하는 기능
c. 동적 구성에는 재패키징이 필요하지 않습니다.
3. 렌더링 시간, 스레드 잠금, GC 시간 등과 같은 주요 시스템 스레드의 함수 호출은 물론 I/O 작업, CPU 로드 및 기타 이벤트를 포함하여 기성 UI 분석 도구를 사용할 수 있습니다.
흐름도
아트메소드 실행 전후 후킹 시 아트메소드의 해석과 실행의 세 가지 상황을 처리하게 된다.
ART 런타임 인터프리터
- 스위치 구조를 기반으로 하는 전통적인 인터프리터인 C++ 인터프리터는 일반적으로 디버깅 환경, 메서드 추적, 명령이 지원되지 않거나 바이트코드에서 예외가 발생할 경우(예: 구조 잠금 확인 실패)에만 이 분기를 사용합니다. .
- mterp 고속 인터프리터의 핵심은 명령어 매핑을 위한 핸들러 테이블을 도입하고, 직접 작성한 어셈블리를 통해 명령어 간의 빠른 전환을 구현하여 인터프리터 성능을 향상시킵니다.
- Nterp는 Mterp의 또 다른 최적화입니다. Nterp는 관리되는 코드 스택을 유지 관리할 필요가 없으며 Native 방식과 동일한 스택 프레임 구조를 사용하며 전체 디코딩 및 변환 실행 프로세스가 어셈블리 코드로 구현되므로 인터프리터와 컴파일된 코드 간의 성능 격차가 더욱 줄어듭니다.
여기서 특이한 점을 발견했습니다. 즉, Android 14의 해석 및 실행이 실제로 스위치 해석 및 실행 방법을 사용한다는 것입니다. 여러 Android 버전의 해석 및 실행 방법을 다시 테스트했습니다. Android 12는 mterp를 사용하고, Android 13은 nterp를 사용하며, 디버깅할 때만 전환됩니다. 이론적으로 Android 14도 가장 느린 스위치를 사용해야 합니다. Backtrace를 실행하기 위한 버전 12, 13, 14의 방법은 다음과 같다.
의심되는 부분을 확인하세요
나는 인터프리터의 실행으로 인해 지연이 발생한다고 의심하기 시작했습니다. 소스 코드
art/runtime/interpreter/mterp/nterp.cc를 살펴보고 실제로 javaDebuggable이라면 변경 사항이 있음을 발견했습니다. nterp를 사용하세요. 다음으로, 이 문제가 발생했음을 증명해 보십시오.
isJavaDebuggable은 Runtime.cc의 RuntimeDebugState Runtime_debug_state_에 의해 제어됩니다. 런타임 인스턴스를 찾고 오프셋을 통해 Runtime_debug_state_ 속성을 수정할 수 있습니다. 소스 코드를 살펴본 후
_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE를 통해 설정할 수도 있습니다.
void Runtime::SetRuntimeDebugState(RuntimeDebugState state) {
if (state != RuntimeDebugState::kJavaDebuggableAtInit) {
// We never change the state if we started as a debuggable runtime.
DCHECK(runtime_debug_state_ != RuntimeDebugState::kJavaDebuggableAtInit);
}
runtime_debug_state_ = state;
}
위 방법으로 확인해 보았는데, 테스트 패키지의 isJavaDebuggable을 false로 설정했는데도 계속 멈췄는데, 프로덕션 패키지의 isJavaDebuggable을 true로 설정했더니 조금 멈췄습니다. 그래서 실행방식 때문에 렉이 발생했다는 추측을 뒤집었습니다.
시간이 많이 걸리는 기본 문제 해결
nativie 메소드를 실행하는 데 시간이 많이 걸리는 것 같습니다. 문제를 찾으려면 다시 simpleperf를 사용해 보세요.
결론: 기본적으로 실행 코드에서 스택을 설명하려면 시간이 많이 걸리고, 그 외에 특별한 스택은 없습니다.
타겟팅
DEBUG_JAVA_DEBUGGABLE
그런 다음 디버깅 가능한 소스부터 시작하여 영향을 미치는 변수를 찾기 위해 범위를 점차적으로 좁혀보세요.
AndroidManifest의 디버그 가능 항목은 시스템 프로세스에 영향을 주어 프로세스에서 RuntimeFlags를 시작합니다.
Frameworks/base/core/java/android/os/Process.java에 있는 시작 메소드의 여섯 번째 매개변수는 RuntimeFlags입니다. debuggableFlag인 경우 다음 플래그와 함께 RuntimeFlags가 추가됩니다. 그런 다음 먼저 레이블 범위를 좁힙니다.
if (debuggableFlag) {
runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;
runtimeFlags |= Zygote.DEBUG_ENABLE_PTRACE;
runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;
// Also turn on CheckJNI for debuggable apps. It's quite
// awkward to turn on otherwise.
runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;
// Check if the developer does not want ART verification
if (android.provider.Settings.Global.getInt(mService.mContext.getContentResolver(),
android.provider.Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, 1) == 0) {
runtimeFlags |= Zygote.DISABLE_VERIFIER;
Slog.w(TAG_PROCESSES, app + ": ART verification disabled");
}
}
프로세스의 시작 매개변수를 수정해야 합니다. 그런 다음 시스템 프로세스를 연결해야 합니다. 여기에는 전화기 루팅, 후크 프레임워크의 일부 작업 설치, 후크 프로세스 시작을 통해 일부 매개변수 수정이 포함됩니다.
hookAllMethods(
Process.class,
"start",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
final String niceName = (String) param.args[1];
final int uid = (int) param.args[2];
final int runtimeFlags = (int) param.args[5];
XposedBridge.log("process_xx " + runtimeFlags);
if (isDebuggable(niceName, user)) {
param.args[5] = runtimeFlags&~DEBUG_JAVA_DEBUGGABLE;
XposedBridge.log("process_xx " + param.args[5]);
}
}
}
);
이번에는 분명한 결과가 있었습니다. DEBUG_JAVA_DEBUGGABLE을 제거한 후 테스트 패키지 런타임 플래그가 더 이상 중단되지 않습니다. DEBUG_JAVA_DEBUGGABLE 마크를 추가한 후 애플리케이션 시장의 애플리케이션을 포함한 프로덕션 패키지가 모두 중단되었습니다. 그러면 DEBUG_JAVA_DEBUGGABLE 변수에 의해 발생했음을 증명할 수 있습니다.
타겟팅
최적화 해제부팅 이미지
DEBUG_JAVA_DEBUGGABLE의 영향을 관찰하려면 소스 코드를 계속 진행하세요.
if ((runtime_flags & DEBUG_JAVA_DEBUGGABLE) != 0) {
runtime->AddCompilerOption("--debuggable");
runtime_flags |= DEBUG_GENERATE_MINI_DEBUG_INFO;
runtime->SetRuntimeDebugState(Runtime::RuntimeDebugState::kJavaDebuggableAtInit);
{
// Deoptimize the boot image as it may be non-debuggable.
ScopedSuspendAll ssa(__FUNCTION__);
runtime->DeoptimizeBootImage();
}
runtime_flags &= ~DEBUG_JAVA_DEBUGGABLE;
needs_non_debuggable_classes = true;
}
여기의 논리는 DEBUG_JAVA_DEBUGGABLE의 영향이며 SetRuntimeDebugState는 이전에 테스트되었습니다.
DEBUG_GENERATE_MINI_DEBUG_INFO의 영향이 아닙니다 . 런타임->DeoptimizeBootImage()인가요? 그래서 debugable이 false인 패키지를 사용하여 _ZN3art7Runtime19DeoptimizeBootImageEv를 통해 DeoptimizeBootImage 메서드를 적극적으로 호출한 후 재현해냈습니다!
원인 분석
DeoptimizeBootImage는 bootImage의 AOT 코드 메소드를 Java 디버깅 가능으로 변환합니다. AOT 코드를 사용하지 않고 메서드 진입점을 다시 초기화하고 해석된 실행으로 이동합니다. Instrumentation::InitializeMethodsCode 메서드를 다시 추적하면
여전히 CanUseNterp(메서드) CanRuntimeUseNterp 지점에 도달합니다. 또한, 안드로이드 13은 nterp를 사용할 수 있고, 안드로이드 14는 스위치만 사용할 수 있습니다.
코드를 다시 연결하고 CanRuntimeUseNterp에 직접 true를 반환하도록 요청했지만 여전히 멈췄습니다. 낚아채도 찾았습니다. 다음 방법은 여전히 해석과 실행을 전환하는 데 사용됩니다. 반대로 생각해보면 내 Hook이 뒤처져 DeoptimizeBootImage가 실행되었기 때문이다. 기본 메서드가 호출되면 스위치가 실행된다.
테스트를 위해 Android 13 디버깅 가능한 true 패키지를 사용했고, 먼저 CanRuntimeUseNterp가 false를 반환하도록 연결한 다음 DeoptimizeBootImage를 실행했는데 지연이 다시 나타났습니다.
예비 위치 지정: bootimage의 메서드는 Android 13에서는 nterp이고 Android 14에서는 스위치 메서드입니다. bootimage의 메서드는 매우 기본적이고 단편화되어 있어 스위치 메서드 실행에 시간이 많이 걸립니다.
인증은 시스템 문제입니다.
시스템 문제라면 누구나 이 문제를 겪게 될 것입니다. 우리 앱뿐만 아니라 디버그 패키지 문제를 확인하는 데 도움을 줄 친구 몇 명을 찾았습니다. 물론, 모두 이 문제를 안고 있습니다. Android 14와 Android 13에 동일한 패키지를 설치한 경험은 완전히 일관되지 않습니다.
피드백 질문
누군가 IssueTracker에 Android 14 디버그 패키지가 느리다고 보고했습니다
(https://issuetracker.google.com/issues/311251587). 하지만 아직 결과가 나오지 않아서 제가 확인한 문제를 보완했습니다.
그런데 저도 문제를 제기했습니다
https://issuetracker.google.com/issues/328477628
3. 임시 해결책
Google의 답변을 기다리는 동안, bootimage에서 메서드를 다시 최적화하는 방법 등 앱 계층에서 이 문제를 피하고 디버그 패키지의 경험을 원활하게 되돌릴 수 있는 방법에 대해서도 생각하고 있습니다. 이 아이디어를 염두에 두고 아트 코드를 다시 연구한 결과 Android 14에 새로운
UpdateEntrypointsForDebuggable 메서드가 추가되었다는 사실을 발견했습니다. 이 메서드는 aot 및 nterp와 같은 규칙에 따라 메서드의 실행 메서드를 재설정한 다음 반환하기 전에 CanRuntimeUseNterp를 연결했습니다. . True UpdateEntrypointsForDebuggable을 다시 호출하면 다시 nterp로 이동하지 않겠습니까?
void Instrumentation::UpdateEntrypointsForDebuggable() {
Runtime* runtime = Runtime::Current();
// If we are transitioning from non-debuggable to debuggable, we patch
// entry points of methods to remove any aot / JITed entry points.
InstallStubsClassVisitor visitor(this);
runtime->GetClassLinker()->VisitClasses(&visitor);
}
위 아이디어대로 해봤는데 훨씬 부드러워졌어요! ! !
사실 위의 해결 방법에는 아직 몇 가지 문제가 남아 있습니다. debugable이 false로 설정된 패키지와 비교하면 여전히 약간의 지연이 있습니다. 또한 bootImage의 메소드가 nterp로 이동했지만 apk의 코드 대부분은 여전히 해석과 실행을 전환하는 데 사용되어 마음이 바뀌었습니다. UpdateEntrypointsForDebuggable을 호출하기 전에 RuntimeDebugState를 디버깅 불가능으로 설정 하고 UpdateEntrypointsForDebuggable을 호출한 후에 RuntimeDebugState를 디버깅 가능으로 설정해
도 괜찮습니까 ? 최종 코드는 다음과 같습니다. Hook Framework는 https://github.com/bytedance/android-inline-hook을 사용합니다.
Java_test_ArtMethodTrace_bootImageNterp(JNIEnv *env,
jclass clazz) {
void *handler = shadowhook_dlopen("libart.so");
instance_ = static_cast<void **>(shadowhook_dlsym(handler, "_ZN3art7Runtime9instance_E"));
jobject
(*getSystemThreadGroup)(void *runtime) =(jobject (*)(void *runtime)) shadowhook_dlsym(handler,
"_ZNK3art7Runtime20GetSystemThreadGroupEv");
void
(*UpdateEntrypointsForDebuggable)(void *instrumentation) = (void (*)(void *i)) shadowhook_dlsym(
handler,
"_ZN3art15instrumentation15Instrumentation30UpdateEntrypointsForDebuggableEv");
if (getSystemThreadGroup == nullptr || UpdateEntrypointsForDebuggable == nullptr) {
LOGE("getSystemThreadGroup failed ");
shadowhook_dlclose(handler);
return;
}
jobject thread_group = getSystemThreadGroup(*instance_);
int vm_offset = findOffset(*instance_, 0, 4000, thread_group);
if (vm_offset < 0) {
LOGE("vm_offset not found ");
shadowhook_dlclose(handler);
return;
}
void (*setRuntimeDebugState)(void *instance_, int r) =(void (*)(void *runtime,
int r)) shadowhook_dlsym(
handler, "_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE");
if (setRuntimeDebugState != nullptr) {
setRuntimeDebugState(*instance_, 0);
}
void *instrumentation = reinterpret_cast<void *>(reinterpret_cast<char *>(*instance_) +
vm_offset - 368 );
UpdateEntrypointsForDebuggable(instrumentation);
setRuntimeDebugState(*instance_, 2);
shadowhook_dlclose(handler);
LOGE("bootImageNterp success");
}
4. 마지막으로
최근 커뮤니티에서 퀄컴 엔지니어의 글을 보았는데, 제가 확인한 문제를 바탕으로 좀 더 자세한 분석을 했고, 구글이 안드로이드 15에서 이 문제를 해결할 것이라고 확인했습니다. 안드로이드 14 기기의 해외 버전이라면 구글은 com.android.artapex 모듈 업데이트를 통해 이 문제를 해결할 계획입니다. 그러나 중국의 네트워크 문제로 인해 구글의 추진이 먹혀들지 못하기 때문에 각 휴대폰 제조사는 이 두 가지 변화를 적극적으로 수용해야 한다. [1]
디버깅 가능한 패키지가 멈추는 문제를 일시적으로 해결해야 하는 경우 위의 방법을 통해서도 해결할 수 있습니다.
참고 기사:
[1] https://juejin.cn/post/7353106089296789556
*문자/ 우유
이 기사는 Dewu Technology의 원본입니다. 더 흥미로운 기사를 보려면 Dewu Technology 공식 웹사이트를 참조하세요.
Dewu Technology의 허가 없이 전재하는 것은 엄격히 금지되어 있으며, 그렇지 않을 경우 법에 따라 법적 책임을 추궁할 것입니다!
Linus는 커널 개발자가 탭을 공백으로 대체하는 것을 막기 위해 문제를 직접 해결했습니다. 그의 아버지는 코드를 작성할 수 있는 몇 안 되는 리더 중 한 명이고, 둘째 아들은 오픈 소스 기술 부서의 책임자이며, 막내 아들은 핵심입니다. Huawei: 일반적으로 사용되는 모바일 애플리케이션 5,000개를 변환하는 데 1년이 걸렸습니다. Hongmeng으로의 포괄적인 마이그레이션 Java는 타사 취약점에 가장 취약한 언어입니다. Hongmeng의 아버지인 Wang Chenglu: 오픈 소스 Hongmeng은 유일한 아키텍처 혁신입니다. 중국 기초 소프트웨어 분야의 마화텅(Ma Huateng)과 저우홍이(Zhou Hongyi)가 악수를 하며 "원한을 풀다" 전 마이크로소프트 개발자: 윈도우 11 성능은 "터무니없을 정도로 나쁘다" 라오샹지가 오픈소스인 것은 코드는 아니지만 그 이유는 다음과 같다. Google이 대규모 구조 조정을 발표 했습니다 .