Azure Monitor & Application Insight to monitor Infrastructure & Application
soscon2018 - Tracing for fun and profit
1. SOSCON 2018
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing the C/C++ open-sources for profit and fun
C/C++ 오픈소스를 Tracing 하면 얻을 수 있는 것들
HANBUM PARK
2. SOSCON 2018
SAMSUNG OPEN SOURCE CONFERENCE 2018
HANBUM PARK
KOSSLAB OPEN-FRONTIER
Uftrace Contributor
Reverse-Engineer
3. SOSCON 2018
SAMSUNG OPEN SOURCE CONFERENCE 2018
The stories I want to share.
• Introduction
• Motivation
• Uftrace
• Tracing for profit
– Android VM
• Tracing for fun
– Gcc
• Retrospect
• Conclusion
CONTENTS
5. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
Native 함수 호출을 통한 연산이 순수 Javascript보다 느린 경우
DOMMatrix perf (https://jsperf.com/dommatrix-perf/9)
JS Obj equivalent
JS equivalent
JS equivalent + DOM access
JS equivalent with floatArray
JS equivalent with floatArray + DOM access
JS with Native-Function call
55
Operation per second (higher is better)
44
43
5
14
32
6. Tracing case study – Node.js
Uftrace로 Tracing 한 후 결과 로그를 통해 이유를 확인할 수 있습니다
Jinho bang - chromium binding 기술을 node.js에 적용해보자 (DEVIEW 2017)
7. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
Sum이란 함수로 배열 안의 값들의 합을 구한다고 가정합시다
let s = sum([1, 2, 3, 4, 5, 6, 7, 8, 9]);
8. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
배열 안의 값들에 합을 구하는 Sum 함수 – Javascript
function sum(elements) {
let s = 0;
elements.forEach(element => { s += element; });
return s;
}
9. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
배열 안의 값들에 합을 구하는 Sum 함수 – Node.js N-API
napi_value Sum(napi_env, napi_callback_info info) {
}
for (int i = 0; i < length; i++) {
napi_value element;
napi_get_element(env, i, &element);
napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
double value;
napi_get_value_double(env, element, &value);
sum += value;
}
napi_value js_sum;
napi_create_double(env, sum, &js_sum);
return js_sum;
10. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
배열 안의 값들에 합을 구하는 Sum 함수는 두 부분으로 이뤄집니다
napi_value Sum(napi_env, napi_callback_info info) {
}
for (int i = 0; i < length; i++) {
napi_value element;
napi_get_element(env, i, &element);
napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
double value;
napi_get_value_double(env, element, &value);
sum += value;
}
napi_value js_sum;
napi_create_double(env, sum, &js_sum);
return js_sum;
연산
반환
11. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
Javascript 단에 저장된 값을 Native에서 사용하기 위해 읽어와야 합니다
napi_value Sum(napi_env, napi_callback_info info) {
}
for (int i = 0; i < length; i++) {
napi_value element;
napi_get_element(env, i, &element);
napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
double value;
napi_get_value_double(env, element, &value);
sum += value;
}
napi_value js_sum;
napi_create_double(env, sum, &js_sum);
return js_sum;
Get Javascript value
napi_value element;
napi_get_element(env, i, &element);
12. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
이 과정에서 Javascript의 값에 대한 Type을 체크해야 합니다
napi_value Sum(napi_env, napi_callback_info info) {
}
for (int i = 0; i < length; i++) {
napi_value element;
napi_get_element(env, i, &element);
napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
double value;
napi_get_value_double(env, element, &value);
sum += value;
}
napi_value js_sum;
napi_create_double(env, sum, &js_sum);
return js_sum;
Type checking
napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
13. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
Native에서 연산이 끝났으면, 결과 값을 Javascript에서 사용가능한 타입으로 바꿔야 합니다
napi_value Sum(napi_env, napi_callback_info info) {
}
for (int i = 0; i < length; i++) {
napi_value element;
napi_get_element(env, i, &element);
napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
double value;
napi_get_value_double(env, element, &value);
sum += value;
}
napi_value js_sum;
napi_create_double(env, sum, &js_sum);
return js_sum; napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
Type converting
14. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
Resolve : Chromium WebIDL to Node.js – automatic binding and checking
// WebIDL
[Constructor]
interface Calculator {
double sum(sequence<long> elements);
};
// Native Code
class Calculator {
public:
double Sum(std::vector<int> elements) {
int sum = 0;
for (int i = 0; I < elements.size(); i++)
sum += elements[i];
return sum;
}
};
// N-API
napi_value Sum(napi_env, napi_callback_info info) {
for (int i = 0; i < length; i++) {
napi_value element;
napi_get_element(env, i, &element);
napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
double value;
napi_get_value_double(env, element, &value);
sum += value;
}
napi_value js_sum;
napi_create_double(env, sum, &js_sum);
return js_sum;
}
15. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
Resolve : Chromium WebIDL to Node.js – ignore type checking
// WebIDL
[Constructor]
interface Calculator {
[NoTypeChecking] double sum(sequence<long> elements);
};
// Native Code
class Calculator {
public:
double Sum(std::vector<int> elements) {
int sum = 0;
for (int i = 0; I < elements.size(); i++)
sum += elements[i];
return sum;
}
};
// N-API
napi_value Sum(napi_env, napi_callback_info info) {
for (int i = 0; i < length; i++) {
napi_value element;
napi_get_element(env, i, &element);
napi_valuetype valuetype;
napi_typeof(env, element, &valuetype);
double value;
napi_get_value_double(env, element, &value);
sum += value;
}
napi_value js_sum;
napi_create_double(env, sum, &js_sum);
return js_sum;
}
16. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
방진호님은 WebIDL을 Node.js에 제의하였으며 개발…
Project Bacardi
https://github.com/lunchclass/bacardi
17. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing case study – Node.js
Tracing, “프로그램의 실행 동안의 변화를 다각도로 기록하고 이를 검토하는 일”
22. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Motivation
Zygote
Android OS
(Managers)
ActivityThread
& Looper
3
attachApplication
4
bindApplication
5
Find Application Class
& Create new Instance
& Call attachBaseContext
6
Fork and Register
PID
1
Application
DalvikVM
JNI_CreateJavaVM
2
Resources
method public static main([Ljava/lang/String;)V
.registers 3
.prologue
.line 19
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello, world!"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 20
return-void
.end method
DEX
DEX의 보호 방안 찾기 대작전!
원본 DEX를 보호하는 메커니즘의 필요
23. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Motivation
You are right always
EntryPoint
Android
Open
Source
Project
24. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Language Files Blank Comment Code Line
C 708 22952 28339 95886
C++ 735 44751 49065 325759
Motivation
Everything grow up, except my salary
AOSP - art
25. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Motivation
분석의 시작은 CC 형제, Ctags + Cscope와 함께 시작했습니다만
LookupClass
ResolveClass
FindClassNoInit
FindClass
DefineClass
LoadClass
LinkClass
ResolveClass
27. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Motivation – time is gold, friends
You are right always, but need time
EntryPoint
Android
Open
Source
Project
28. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Motivation – time is gold, friends
Reverse Engineer을 하는 일반적인 방식 #1
Process
Disk
Network
Memory
Binary
API
29. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Motivation – time is gold, friends
Reverse Engineer을 하는 일반적인 방식 #2
Process
Disk
Network
Memory
Binary
API
Logging
Monitoring
Record
30. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Motivation – time is gold, friends
Reverse Engineer을 하는 일반적인 방식 #3
Process
Disk
Network
Memory
API
Record
Binary
API
Catch
Event
Logging
Monitoring
FOUND
Logic
!!
31. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Motivation – time is gold, friends
프로그램의 실행을 흐름을 로깅하면 분석에 필요한 최소 로직을 추려낼 수 있습니다
Android
Open
Source
Project
Binary
API
dalvikvm
33. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Introduce UFTRACE
일반적인 컴파일 결과
void bar() {
}
void foo() {
bar();
}
int main() {
foo();
}
<bar>:
ret
<foo>:
call <bar>
ret
<main>:
call <foo>
ret
$ gcc foobar.c
34. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Introduce UFTRACE
-pg 옵션으로 컴파일 했을 때의 결과
void bar() {
}
void foo() {
bar();
}
int main() {
foo();
}
<bar>:
call <mcount@plt>
ret
<foo>:
call <mcount@plt>
call <bar>
ret
<main>:
call <mcount@plt>
call <foo>
ret
$ gcc –pg foobar.c
35. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Introduce UFTRACE
-pg로 컴파일 하면 컴파일러가 mcount 호출을 생성해 줍니다
<bar>:
call <mcount@plt>
ret
<foo>:
call <mcount@plt>
call <bar>
ret
<main>:
call <mcount@plt>
call <foo>
ret
<mcount@plt>:
# record callee position
ret
36. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Introduce UFTRACE
함수 간에는 인자 값을 전달하는 방식은 약속되어 있습니다
void bar(int arg) {
}
void foo(int arg) {
bar(arg);
}
int main() {
foo(0x10);
}
<bar>:
# 인자 값 받기
ret
<foo>:
# 인자 값 받기
call <bar> #인자 보내기
ret
<main>:
call <foo> #인자 보내기
ret
$ gcc foobar.c -o output
37. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Introduce UFTRACE
함수 간에는 인자 값을 전달하는 방식은 약속되어 있습니다
void bar(int arg) {
}
void foo(int arg) {
bar(arg);
}
int main() {
foo(0x10);
}
<bar>:
# 인자 값 받기
ret
<foo>:
# 인자 값 받기
call <bar> #인자 보내기
ret
<main>:
call <foo> #인자 보내기
ret
$ gcc foobar.c -o output
Calling Convention
- 인자 값 전달
- 호출 지점으로 복귀
38. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Introduce UFTRACE
호출 규약에 지정된 레지스터, 메모리를 읽어 인자 값을 알아낼 수 있습니다
<bar>:
# 인자 값 받기
ret
<foo>:
# 인자 값 받기
call <bar> #인자 보내기
ret
<main>:
call <foo> #인자 보내기
ret
Call <mcount@plt>
Call <mcount@plt>
Uftrace
# 함수 호출 기록
# 인자 값 저장하기 대작전
$ gcc foobar.c -o output
39. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
LLDB with python script : 0.277480 sec
Uftrace record : 0.000532 sec
Compile with –pg : 0.000092 sec
Native : 0.000010 sec
Uftrace performance
동일 작업을 한다고 가정하면, Debugger보다 부하가 월등히 적습니다
Uftrace record with python script : 0.005210 sec
Appendix A. benchmark
44. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing for profit
Hello World를 출력하는 간단한 샘플 프로그램을 작성하여 Tracing!
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
$ javac Main.java
$ dx --dex --output=classes.dex Main.class
$ zip Main.apk classes.dex
$ uftrace record --auto-args `which dalvikvm64` –cp Main.apk Main
45. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing for profit
Results : 1,144,484 lines function call record
1144476 0.088 us [ 99631] | art::FaultManager::~FaultManager();
1144477 0.055 us [ 99631] | std::__1::__vector_base::~__vector_base();
1144478 0.060 us [ 99631] | std::__1::__vector_base::~__vector_base();
1144479 0.054 us [ 99631] | art::JDWP::JdwpOptions::~JdwpOptions();
1144480 0.052 us [ 99631] | std::__1::__vector_base::~__vector_base();
1144481 0.078 us [ 99631] | std::__1::__vector_base::~__vector_base();
1144482 0.053 us [ 99631] | std::__1::unique_ptr::~unique_ptr();
1144483 0.063 us [ 99631] | std::__1::unique_ptr::~unique_ptr();
1144484 0.150 us [ 99631] | std::__1::unique_ptr::~unique_ptr();
1 # DURATION TID FUNCTION
2 [ 99631] | main(4, 0x7ffc2b7a7bb8) {
3 0.595 us [ 99631] | setvbuf();
4 0.922 us [ 99631] | operator new[](48) = 0x55b99858f5b0;
5 0.415 us [ 99631] | memset(0x55b99858f5b0, 0, 48) = 0x55b99858f5b0;
6 147.427 us [ 99631] | strncmp("-cp", "-XXlib:", 7) = 11;
7 0.305 us [ 99631] | strcmp("-cp", "-classpath") = 4;
8 0.166 us [ 99631] | strcmp("-cp", "-cp") = 0;
9 0.198 us [ 99631] | strncmp("Main.apk", "-XXlib:", 7) = 32;
10 0.146 us [ 99631] | strcmp("Main.apk", "-classpath") = 32;
11 0.111 us [ 99631] | strcmp("Main.apk", "-cp") = 32;
56. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
FindClassNoInit
ResolveClass
FindSystemClassNoInit
FindClassNoInit
DexFindClass
LoadClassFromDex
AddClassToHash
LinkClass
LookupClass
Resolved Class
Class
Class
Linking
FindClass
Load Class from
Specific ClassLoader
Interpreter
DefineClass
From
DexFile
Find Class
from Dex
Directly
Tracing for profit
다양한 클래스 생성 과정
57. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing for profit
Class를 로드하는 Path를 Tracing 해냈습니다!
INTERPRETER
CLASSLOADER
Android VM
HEAP DATA
Tables
Global Area
Class
Loader
Class
Loader
Loaded
Classes
Class
Loader
Find Class
From
Loaded Classes
1
vtable
ClassObject
static field
iftrable
directMethods
virtualMethods
Method Area
Method
Method
Dex Header
String_ids
Type_ids
Proto_ids
Field_ids
Method_ids
Class_defs
Data
DexFile
Format
Dex
OpenDex
2
DexParse
3
LoadClass
4
Add Class Hash
To
Loaded Classes
5
Parse ClassDef
Load Data
4-1
4-2
Method
Instructions
58. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing for profit
Hello world!
# direct methods
method public constructor <init>()V
.registers 1
.prologue
.line 17
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
method public static main([Ljava/lang/String;)V
.registers 3
.prologue
.line 19
sget-object v0,
Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello, world!"
invoke-virtual {v0, v1},
Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 20
return-void
.end method
ScopedLocalRef<jclass> klass(env, env->FindClass(class_name.c_str()));
if (klass.get() == nullptr) {
fprintf(stderr, "Unable to locate class '%s'n", class_name.c_str());
env->ExceptionDescribe();
return EXIT_FAILURE;
}
jmethodID method = env->GetStaticMethodID(klass.get(), "main",
"([Ljava/lang/String;)V");
if (method == nullptr) {
fprintf(stderr, "Unable to find static main(String[]) in '%s'n",
class_name.c_str());
env->ExceptionDescribe();
return EXIT_FAILURE;
}
// Make sure the method is public. JNI doesn't prevent us from
// calling a private method, so we have to check it explicitly.
if (!IsMethodPublic(env, klass.get(), method)) {
fprintf(stderr, "Sorry, main() is not public in '%s'n",
class_name.c_str());
env->ExceptionDescribe();
return EXIT_FAILURE;
}
63. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
코드를 변경해야 하나? #1 – 문자열 포인터 반환으로 변경
static inline __attribute__((always_inline)) char *get_msg()
{
char *str = "Hello worldn";
return str;
}
int main()
{
char *p;
p = get_msg();
puts(p);
return 0;
}
64. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
코드를 변경해야 하나? #1 – 문자열 포인터를 반환하게 바꾸면 내용을 수정 못함
static inline __attribute__((always_inline)) char *get_msg()
{
char *str = "Hello worldn";
return str;
}
int main()
{
char *p;
p = get_msg();
p[1] = 'a';
puts(p);
return 0;
}
$ ./compile_by_clang
Segmentation fault (core dumped)
$ ./compile_by_gcc
Segmentation fault (core dumped)
65. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
코드를 변경해야 하나? #1 – “Hallo world”를 출력하고 싶은데요...
static inline __attribute__((always_inline)) char *get_msg()
{
char *str = "Hello worldn";
return str;
}
int main()
{
char *p;
p = get_msg();
p[1] = 'a';
puts(p);
return 0;
}
p[1] = 'a';
66. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
static inline __attribute__((always_inline)) char *get_msg()
{
char *str = "Hello worldn";
return str;
}
int main()
{
char *p;
p = get_msg();
p[1] = 'a';
puts(p);
return 0;
}
Tracing For Fun
코드를 변경해야 하나? #1 – 문자열은 Const Char * 형으로 Data Section에 저장
$ objdump -s compile_by_gcc
...
Contents of section .rodata:
4005f0 01000200 48656c6c 6f20776f 726c640a ....Hello world.
400600 00
…
Const char *
67. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
static inline __attribute__((always_inline)) char *get_msg()
{
const char *str = "Hello worldn";
char *p = malloc(strlen(str));
strcpy(p, str);
return p;
}
int main()
{
char *p;
p = get_msg();
puts(p);
free(p);
return 0;
}
Tracing For Fun
코드를 변경해야 하나? #2 – 동적 할당
동적 할당으로 변경하면
해제를 해줘야...
68. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
재미로 원인 추적하기 - Warning 메시지를 따라가보자
$ gcc inline.c
inline.c: In function ‘get_msg:
inline.c:6:9: warning: function returns address of local variable [-Wreturn-local-addr]
return (char *)str;
69. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
I love warning message.
10105 tree
10106 c_finish_return (location_t loc, tree retval, tree origtype)
...
10227 case ADDR_EXPR:
10228 inner = TREE_OPERAND (inner, 0);
10229
10230 while (REFERENCE_CLASS_P (inner)
10231 && !INDIRECT_REF_P (inner))
10232 inner = TREE_OPERAND (inner, 0);
10233
10234 if (DECL_P (inner)
10235 && !DECL_EXTERNAL (inner)
10236 && !TREE_STATIC (inner)
10237 && DECL_CONTEXT (inner) == current_function_decl)
10238 {
10239 if (TREE_CODE (inner) == LABEL_DECL)
10240 warning_at (loc, OPT_Wreturn_local_addr,
10241 "function returns address of label");
10242 else
10243 {
10244 warning_at (loc, OPT_Wreturn_local_addr,
10245 "function returns address of local variable");
10246 tree zero = build_zero_cst (TREE_TYPE (res));
10247 t = build2 (COMPOUND_EXPR, TREE_TYPE (res), t, zero);
10248 }
10249 }
10250 break;
...
70. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
static inline __attribute__((always_inline))
char *get_msg()
{
char str[64] = "Hello worldn";
}
return
return statement
(char *)str;
operand
Address
Expression?
Valid
Declaration?
!External?
!Static?
Local?
이건 0이 딱이네!
Tracing For Fun
str이 return 시 0이 되는 이유가 이렇습니다.
71. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
I love warning message.
10105 tree
10106 c_finish_return (location_t loc, tree retval, tree origtype)
...
10227 case ADDR_EXPR:
10228 inner = TREE_OPERAND (inner, 0);
10229
10230 while (REFERENCE_CLASS_P (inner)
10231 && !INDIRECT_REF_P (inner))
10232 inner = TREE_OPERAND (inner, 0);
10233
10234 if (DECL_P (inner)
10235 && !DECL_EXTERNAL (inner)
10236 && !TREE_STATIC (inner)
10237 && DECL_CONTEXT (inner) == current_function_decl)
10238 {
10239 if (TREE_CODE (inner) == LABEL_DECL)
10240 warning_at (loc, OPT_Wreturn_local_addr,
10241 "function returns address of label");
10242 else
10243 {
10244 warning_at (loc, OPT_Wreturn_local_addr,
10245 "function returns address of local variable");
10246 tree zero = build_zero_cst (TREE_TYPE (res));
10247 t = build2 (COMPOUND_EXPR, TREE_TYPE (res), t, zero);
10248 }
10249 }
10250 break;
...
mov $0x0,%eax
mov %rax,-0x8(%rbp)
mov -0x8(%rbp),%rax
mov %rax,%rdi
79. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
$ gcc –O1 –fdump-tree-all-details inline.c
;; Function get_msg (get_msg, funcdef_no=0, decl_uid=1792, cgraph_uid=0, symbol_order=0)
Deleted dead store: str = "Hello worldn";
__attribute__((always_inline)) get_msg ()
{
char str[64];
<bb 2> [0.00%]:
str ={v} {CLOBBER};
return &str;
}
;; Function main (main, funcdef_no=1, decl_uid=1796, cgraph_uid=1, symbol_order=1)
main ()
{
char str[64];
char * D.1810;
char * p;
<bb 2> [100.00%]:
str ={v} {CLOBBER};
puts (&str);
return 0;
}
80. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
;; Function get_msg (get_msg, funcdef_no=0, decl_uid=1792, cgraph_uid=0, symbol_order=0)
Deleted dead store: str = "Hello worldn";
__attribute__((always_inline)) get_msg ()
{
char str[64];
<bb 2> [0.00%]:
str ={v} {CLOBBER};
return &str;
}
;; Function main (main, funcdef_no=1, decl_uid=1796, cgraph_uid=1, symbol_order=1)
main ()
{
char str[64];
char * D.1810;
char * p;
<bb 2> [100.00%]:
str ={v} {CLOBBER};
puts (&str);
return 0;
}
Tracing For Fun
$ gcc –O1 –fdump-tree-all-details inline.c
inline.c.040t.dse1
Deleted dead store: str = "Hello worldn";
81. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
함수 인자 값을 함께 기록하도록 옵션을 지정하여 Tracing 해봅니다
$ uftrace record --auto-args -A fwrite@arg1/s
`which gcc` -O1 -fdump-tree-all-details
inline.c
82. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
Tracing 결과에서 Deleted dead store 검색하면 위치를 찾을 수 있습니다
$ uftrace replay > replay.log
$ wc -lc replay.log
4157866 line 392760323 charater replay.log
$ grep "Deleted" -B2 replay.log
[110797] | delete_dead_assignment(0x7ffe0825b230) {
[110797] | gsi_stmt();
[110797] | fwrite(" Deleted dead store: ") = 22;
83. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
Uftrace TUI를 통한 소스 브라우징! - delete_dead_assignment 검색
85. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
소스코드를 찾을 수 있을 때 까지 U키를 눌러 상위 함수로 이동합니다
dse_dom_walker::dse_optimize_stmt
delete_dead_assignment
86. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
한 번 더 하면, pass_dse 에 도달하게 됩니다
_GLOBAL__N_1::pass_dse::execute
dse_dom_walker::dse_optimize_stmt
delete_dead_assignment
87. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
/* This file implements dead store elimination.
A dead store is a store into a memory location which will later be
overwritten by another store without any intervening loads. In this
case the earlier store can be deleted.
In our SSA + virtual operand world we use immediate uses of virtual
operands to detect dead stores. If a store's virtual definition
is used precisely once by a later store to the same location which
post dominates the first store, then the first store is dead.
The single use of the store's virtual definition ensures that
there are no intervening aliased loads and the requirement that
the second load post dominate the first ensures that if the earlier
store executes, then the later stores will execute before the function
exits.
It may help to think of this as first moving the earlier store to
the point immediately before the later store. Again, the single
use of the virtual definition and the post-dominance relationship
ensure that such movement would be safe. Clearly if there are
back to back stores, then the second is redundant.
Reviewing section 10.7.2 in Morgan's "Building an Optimizing Compiler"
may also help in understanding this code since it discusses the
relationship between dead store and redundant load elimination. In
fact, they are the same transformation applied to different views of
the CFG. */
tree-ssa-dse.c
DSE(Dead Store Elimination)
88. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
최적화 때문이라면 최적화를 하지 못하게 강제하고 비교/확인해 볼까요?
static char *get_msg()
{
char str[64] = "Hello worldn";
return (char *)str;
}
int main()
{
char *p;
p = get_msg();
puts(p);
return 0;
}
89. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
최적화 때문이라면 최적화를 하지 못하게 강제하고 비교/확인해 볼까요?
static char *get_msg()
{
volatile char str[64] = "Hello worldn";
return (char *)str;
}
int main()
{
char *p;
p = get_msg();
puts(p);
return 0;
}
static char *get_msg()
{
char str[64] = "Hello worldn";
return (char *)str;
}
int main()
{
char *p;
p = get_msg();
puts(p);
return 0;
}
volatile
메모리 최적화 방지 메모리 최적화
90. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
$ gcc –O1 –fdump-tree-all-details inline.c
;; Function get_msg (get_msg, funcdef_no=0)
get_msg ()
{
volatile char str[64];
<bb 2> [0.00%]:
str ={v} "Hello worldn";
return 0B;
}
;; Function main (main, funcdef_no=1)
main ()
{
char * p;
<bb 2> [0.00%]:
p_3 = get_msg ();
puts (p_3);
return 0;
}
;; Function get_msg (get_msg, funcdef_no=0)
Deleted dead store: str = "Hello worldn";
get_msg ()
{
char str[64];
<bb 2> [0.00%]:
str ={v} {CLOBBER};
return 0B;
}
;; Function main (main, funcdef_no=1)
main ()
{
char * p;
<bb 2> [0.00%]:
p_3 = get_msg ();
puts (p_3);
return 0;
}
메모리 최적화 방지 메모리 최적화
inline.c.040t.dse1
Deleted dead store: str = "Hello worldn";
91. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
$ uftrace graph -D 2 -F dse_dom_walker::dse_optimize_stmt -f none
메모리 최적화 방지 메모리 최적화
92. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Tracing For Fun
10227 case ADDR_EXPR:
10228 inner = TREE_OPERAND (inner, 0);
10229
10230 while (REFERENCE_CLASS_P (inner)
10231 && !INDIRECT_REF_P (inner))
10232 inner = TREE_OPERAND (inner, 0);
10233
10234 if (DECL_P (inner)
10235 && !DECL_EXTERNAL (inner)
10236 && !TREE_STATIC (inner)
10237 && DECL_CONTEXT (inner) == current_function_decl)
10238 {
10239 if (TREE_CODE (inner) == LABEL_DECL)
10240 warning_at (loc, OPT_Wreturn_local_addr,
10241 "function returns address of label");
10242 else if (DECL_DECLARED_INLINE_P(current_function_decl))
10243 inner->base.volatile_flag = true;
10244 else
10245 {
10246 warning_at (loc, OPT_Wreturn_local_addr,
10247 "function returns address of local variable");
10248 tree zero = build_zero_cst (TREE_TYPE (res));
10249 t = build2 (COMPOUND_EXPR, TREE_TYPE (res), t, zero);
10250 }
10251 }
10252 break;
else if (DECL_DECLARED_INLINE_P(current_function_decl))
inner->base.volatile_flag = true;
return 될 tree가 속한 함수가
inline 으로 선언되어 있으면
0으로 바꾸지 마세요!
+ volatile_flag true set
95. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Retrospect
문외한도 아는 척 할 수 있는 핵심은 – 가독성!
;; Function get_msg (get_msg, funcdef_no=0, decl_uid=1792, cgraph_uid=0, symbol_order=0)
Deleted dead store: str = "Hello worldn";
__attribute__((always_inline)) get_msg ()
{
char str[64];
<bb 2> [0.00%]:
str ={v} {CLOBBER};
return &str;
}
;; Function main (main, funcdef_no=1, decl_uid=1796, cgraph_uid=1, symbol_order=1)
main ()
{
char str[64];
char * D.1810;
char * p;
<bb 2> [100.00%]:
str ={v} {CLOBBER};
puts (&str);
return 0;
}
96. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
;; Function get_msg (get_msg, funcdef_no=0, decl_uid=1792, cgraph_uid=0, symbol_order=0)
Deleted dead store: str = "Hello worldn";
__attribute__((always_inline)) get_msg ()
{
char str[64];
<bb 2> [0.00%]:
str ={v} {CLOBBER};
return &str;
}
;; Function main (main, funcdef_no=1, decl_uid=1796, cgraph_uid=1, symbol_order=1)
main ()
{
char str[64];
char * D.1810;
char * p;
<bb 2> [100.00%]:
str ={v} {CLOBBER};
puts (&str);
return 0;
}
Retrospect
문외한도 아는 척 할 수 있는 핵심은 – 가독성!
(gdb) p *gsi->ptr
$4 = {code = GIMPLE_ASSIGN, no_warning = 0, visited = 0, nontemporal_move = 0, plf
= 1, modified = 0, has_volatile_ops = 0, pad = 0, subcode = 32, uid = 0,
location = 2147483655, num_ops = 2, bb = 0x7ffff6ede478, next = 0x7ffff702d190,
prev = 0x7ffff702d140}
97. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Retrospect
Gcc를 수정하는 과정에서 관련 지식도 조금 습득했습니다
else if (DECL_DECLARED_INLINE_P(current_function_decl))
inner->base.volatile_flag = true;
GIMPLE RTL
TREE
Abstract IR
Front-End Back-End
Front-end Optimize Pass :
DSE
Volatile
CLOBBER
CONSTRUCOR
98. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Retrospect
Android VM에 대한 부분 부분의 지식들이
VM
Interpreter
Bytecode
동작 방식에
대한 이해
DEX 파일
포맷에
대한 지식
Bytecode에
대한 지식
99. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Retrospect
Tracing의 과정을 거치면서 연결되게 되면
VM
Interpreter
Bytecode
동작 방식에
대한 이해
DEX 파일
포맷에
대한 지식
Bytecode에
대한 지식
100. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Retrospect
답답하기만 했던 코드가 이해되는 시원함을 느끼게 됩니다
VM
Interpreter
Bytecode
동작 방식에
대한 이해
DEX 파일
포맷에
대한 지식
Bytecode에
대한 지식
103. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Conclusion
오픈소스의 접근성 올리기
Open-source tracing data
API
정보 #1
정보 #2
정보 #3
104. SOSCON
SAMSUNG OPEN SOURCE CONFERENCE 2018
Conclusion
오픈소스의 접근성 올리기
Global Declared Classes
Optimization step The actual step to run dse.
The optimization is actually performed when this function is called
A dead store is a store into a memory location which will later be
overwritten by another store without any intervening loads. In this
case the earlier store can be deleted.
=====================================================
[ 32314] | _GLOBAL__N_1::pass_dse::pass_dse() {
29.798 us [ 32314] | } /* _GLOBAL__N_1::pass_dse::pass_dse */
$ uftrace record -S clarity_gcc_opt_path.py `which gcc` -O1 inline.c
제가 공유하고 싶은 이야기는 tracing에 대한 소개와
제가 어쩐 연유로 tracing에 관심을 갖게 됐는지를 말씀드리고
트레이싱 툴로서 Uftrace를 간단히 소개드린 다음
Uftrace를 활용해 트레이싱했던 경험을 공유하고
그를 고찰하여 내린 결론을 말씀드리는 순서로 진행될겁니다.
Javascript 엔진들은 여러 이유가 있겠지만
그 중 특히 성능 상의 이점 때문에
C나 C++로 짜여진 네이티브 함수를 호출하는 인터페이스를 제공합니다.
근데 특정 경우에는 네이티브 함수를 호출하는 것 보다 순수 자바스크립트에서
연산하는게 오히려 더 빠를 때가 있다는 겁니다.
왜 네이티브 함수 호출을 통한 연산이 순수 자바스크립트보다 느릴 수가 있냐면
자바스크립트에서 네이티브 함수 호출을 하기 위한 몇 가지 절차가 함수 호출 때마다 일어나
성능을 저하시키기 때문입니다.
그림은 그를 트레이싱한 결과인데요, Native Type으로의 전환을 위해 타입 체킹이 반복적으로
일어나는 것을 확인 하실 수 있으실 겁니다. IsNumber라는 텍스트가 보이시죠?
한번 배열 안의 값들을 구하는 sum이란 함수를 구한다고 예를 들어보겠습니다.
자바스크립트로 작성한 코드입니다.
더 설명할 것도 없겠죠?
근데 이를 Native에서 연산하기 위해 Node.js의 N-API를 활용해
코드를 작성하면 꽤나 설명이 필요해질 것 같습니다.
이 코드는 크게 연산이 이뤄지는 부분과 반환하는 부분으로 나눠 볼 수 있는데요
연산하는 부분에서 호출한 자바스크립트가 넘겨준 값을 읽어와야하구요
값의 타입이 무엇인지 확인을 해야합니다.
확인된 타입으로 타입을 변환도 해줘야 하죠.
이런 과정을 거치고 나서야 합을 구할 수 있고,
마찬가지로 반환을 해줄 수 있는 겁니다.
이런 절차가 자바스크립트에서 네이티브 함수 호출을 하는 경우에
반복적으로 일어나기 때문에 자바스크립트에서 연산하는게
더 빠른 경우도 발생하는 겁니다.
이 문제를 파악한 코스랩에서 활동하시는 방진호님은
크로미움에 WebIDL이라는 기술을 Node.js에서 제안했구요.
이 기술은 자동으로 javascript 값의 타입을 검사하고 IDL에 기술된 타입으로
전환해주기 때문에 코드가 좀더 간결해집니다.
필요하지 않은 경우에 타입을 검사하지 않아서
그만큼의 성능상 이점을 가져갈 수 있게 됩니다.
현재 node.js에 제안이 받아들여져 bacardi라는 이름으로
개발 하시는 것으로 알고 있습니다.
이제 트레이싱이 무엇인지 감이 오시나요?
트레이싱이라는 것은 프로그램이 실행되는 동안의 변화를 다각도록 기록하고 이를 검토하는 것을 말합니다.
이번 예에서 방진호님은 트레이싱을 통해 문제를 파악하고 해결방안을 제시하실 수 있었죠.
이제 제 경험을 소개드릴텐데 앞서 보신 것보다 좀더 친숙하게 느껴지실 거라고 기대합니다.
제가 왜 Tracing에 관심을 갖게 되었냐면,
사실 앞서 소개드린 케이스를 먼저 접했지만 그때는 크게 와닿지는 않았었거든요.
근데 회사에서 안드로이드 가상머신을 분석하게 되버린거죠.
안드로이드에서 새 앱이 구동되는 순서는 대략 이렇습니다.
새 앱이 구동 될 때마다 가상 머신을 만들면서
APK라는 안드로이드 패키지를 인자로 전달하게 됩니다.
이 APK는 DEX라는 안드로이드 실행파일과 기타 리소스로 구성되어 있는데
문제는 DEX라는 안드로이드 실행파일 포맷이 너무 쉽게 리버싱되서
원본 소스를 복원해내기 쉽다는 겁니다.
여러분이 보시는 코드는 스몰리라고 DEX가 바이트코드로 이루어져 있는데
바이트코드를 사람이 읽을 수 있는 텍스트로 출력을 해준 것 뿐입니다.
그럼에도 불구하고 대충 뭔지 알 수 있지 않나요?
헬로 월드를 출력해주는 프로그램 입니다.
그런 이유로 DEX를 보호하는 방안이 필요했고,
저희는 경쟁사보다 우위를 점하기 위해 향상된 기법을 찾고자 했습니다.
그런 이유로 제가 안드로이드 가상머신을 분석하게 되버린거죠.
만약 누가 여러분에게 월급을 주면서 오픈소스를 분석 해달라고 하면 하시겠습니까? 당시의 저는 하고 싶었습니다.
과거의 저에게 stay라고 말해주고 싶네요.
당시에는 오픈소스 분석이 뭐… 어려워봐야 얼마나 어렵겠냐는 생각이었습니다.
시작지점부터 분석해가면 끝나긴 할거 아닌가? 라고 생각했죠.
하지만 어리석은 생각이더군요.
모든 것이 성장합니다. 오픈소스도 마찬가지구요. 제 월급만 빼구요.
안드로이드 소스코드도 양이 40만 라인정도로 상당했습니다.
안드로이드 가상머신 분석에 돌입하여 ctags, cscope를 활용해 시작지점부터
분석해 들어가면서 호출 흐름을 이면지에 적어 기록하기 시작했습니다.
그렇게 적기 시작한 이면지가 책상에 점점 쌓여가는데요.
보시다시피 특정 지점 이후로 어디를 분석해야 할지 갈피를 잡지 못하는
상황에 직면했습니다.
시작지점부터 분석해 들어가는 것이 꼭 나쁜 것은 아닙니다.
다만 시간이 좀 필요했을 뿐이고 저한테는 없을 뿐인거죠.
이런 방식으로는 끝이 없겠다는 생각이 들어 소스코드의 정적 분석을 끝내고
다른 방식으로 접근해보기 위해 역공학을 했던 기억을 되살려 봤습니다.
역공학은 주로 바이너리를 대상으로 하기 때문에
분석이 필요한 지점을 찾아 범위를 한정하는데 많은 공을 들입니다.
어떤 방식으로 범위를 줄일 수 있느냐면,
바이너리의 API호출을 후킹등의 방법을 통해 기록하고,
동시에 시스템 자원들의 실시간 모니터링 내용을 함께 기록하면서
특이한 사항이 있는지 분석할 필요가 있는지를 추려내는 것이죠.
트레이싱과 많이 유사합니다.
다만, 역공학을 통해 주로 찾고자 하는 것은 프로그램의 이상 동작이라는 차이가 있습니다.
하지만 원하는 로직을 찾아서
분석해야 할 작업의 양을 줄이는 것은
트레이싱과 동일한 목표입니다.
다시 말해 바이너리의 실행을 로깅하여 이를 추적하면,
우선적으로 실행에 필요한 소스와 로직을 추릴 수 있을거라
생각이 들었습니다.
그래서, 트레이싱을 해줄 적절한 툴 부터 찾았는데요,
저는 Uftrace를 사용하였습니다. Uftrace는 트레이싱에 좋은 기능들이 많습니다.
아주 빠르고 간단하게 uftrace에 대해 설명드리겠습니다.
먼저 Uftrace가 tracing을 하는 메커니즘을 설명드릴건데요.
예를 들어 왼쪽 소스코드를 컴파일 하면 함수 호출이
오른쪽처럼 call과 ret이라는 어셈블리 코드로 컴파일 될 겁니다.
여기서 –pg 옵션을 줘서 컴파일하면 컴파일러가 각 함수마다 시작지점에
Mcount라는 이름의 함수를 호출하도록 코드를 삽입 해놓습니다.
그러면 각 함수 호출시마다 함수의 시작 지점에서 mcount가 우선적으로 호출하게 되니
mcount라는 함수에서는 이를 기록하면 함수들의 호출 순서를 기록할 수 있게 되는 겁니다.
여기에 추가로 함수를 호출할 때는 정해진 규칙이 몇 가지 있는데요.
그 중에 인자 값을 전달하는 규칙이 있습니다.
이것을 콜링 컨벤션, 함수 호출 규칙이라고 부르며
정해진 방식으로 인자를 전달하고 받을 수 있도록
상호간에 약속된 규칙입니다.
Uftrace에서는 함수 호출 규칙을 활용하여
함수 호출 뿐 아니라 함수에 전달되는 인자도 함께 기록하는
기능이 있습니다.
이런 방식은 Native하게 이뤄지기 때문에 동일한 방식을 디버거로 구현했을 때에 비해
부하가 적다는 장점이 있습니다.
Uftrace는 트레이싱 된 결과에 대해서 시각적 가공도 지원하는데요,
보시는 그림은 flamegraph라고 부릅니다.
커널에서는 perf 라는 성능 측정을 위한 기능이 있는데 perf를 통해 나온 결과를
시각화 해서 보고자 할 때 많이 사용되는 그래프 입니다.
Chromium에도 자체 프로파일링 및 트레이싱 기능이 있고 트레이싱 데이터를 시각화 해서 볼 수 있는
트레이싱 뷰어도 제공합니다.
Uftrace에서도 해당 데이터 포맷을 지원하기 때문에
chrome tracing 뷰어에서 볼 수 있도록 데이터 가공할 수 있습니다.
DOT는 graph를 표현하기 위한 언어인데 이를 기반으로 graph를 그려주는 툴들이 많습니다.
Uftrace는 DOT형태로도 데이터를 가공해줍니다.
이제 Uftrace로 Android 가상머신을 트레이싱 해본 내용을 말씀드리려고 합니다
안드로이드 소스를 다운 받아 컴파일하면 Dalvikvm이라는 이름의 빌드 환경에서 실행할 수 있는 바이너리가 생성됩니다.
이를 통해 uftrace로 안드로이드 가상머신을 트레이싱 해볼 수 있습니다.
트레이싱을 해보기 위해 간단히 Hello world를 출력해주는 프로그램을 작성하여
DX라는 안드로이드 유틸을 사용하여 DEX포맷으로 변환하고 이를 Main.apk로 압축하면
준비는 끝입니다.
마지막 줄처럼 uftrace로 dalvikvm을 실행하면 트레이싱을 하게 되는거죠.
트레이싱 해보면 약 100만줄 정도의 함수 호출 기록을 얻을 수 있구요.
이를 시각화하면 chrome tracing 뷰어는 보시는 것처럼 보여줍니다.
아무래도 chrome 트레이싱 뷰어로 보기는 좀 별로일것 같죠?
다음으로 flamegraph를 통해 결과를 확인해봤습니다.
텍스트가 좀 보이는게 마음이 편해지지 않나요?
이렇게 많은 트레이싱 결과가 나오더라도 당황할 필요가 없습니다.
어차피 중요 로직들은 반복적으로 호출되기 때문에 DEX라는 실행파일을 입력받아
DEX에 들어있는 Hello world를 출력하는 과정은 분명 찾을 수 있을 것이기 때문입니다.
플레임 그래프를 쭉 훑어보면, 동일한 함수 이름이 반복해서 보이는데요
혹시 확인하실 수 있는지 모르겠는데, FindClass나 interpreter가 들어가는 함수들이 그렇습니다.
FindClass와 Interpreter로 검색을 해보면 Interpreter가 56%, find class가 36%의 지분을 가지고
있다는 걸 알 수 있습니다.
플레임 그래프는 특정 노드를 클릭하면 클릭 지점 “하”에서
발생하는 호출 흐름으로 축소해서 보여줍니다.
이제 좀더 자세히 보이실테니 몇 개의 텍스트가 반복 된다는걸
유심히 보시면 아실 수 있으실 겁니다.
이렇게 플레임그래프를 탐색하고 클릭 몇 번을 하여
특정 호출 흐름을 추려낼 수 있습니다.
추려낸 함수는 MterpInvokeDirect라는 이름의
Interpreter가 인스트럭션을 처리하는 함수 중에 하나입니다.
Uftrace의 기능 중에 tui를 사용하면 터미널 환경에서 함수 호출 흐름을
그래프로 확인할 수 있습니다. 또한 소스 브라우징도 할 수 있습니다.
MterpInvokeDirect 함수를 검색해보면, 소스를 추적해 들어가면
이는 super를 호출했을 때 즉, 부모 클래스의 메소드를 호출하는 함수라는 것을 알 수 있습니다.
이런식으로 MterpInvoke 명령어를 몇 개 찾아보면 인터프리팅이 바이트코드와 매핑되어
바이트코드마다 호출되게 되어 있는 MterpInvoke로 시작하는 함수들이 있다는 것을
확인할 수 있었습니다.
이번에는 자바 기반 가상 머신에서 핵심인 class가 로드되는 구간을 찾아보겠습니다.
마찬가지로 플레임 그래프에서 FindClass를 검색 후
클릭해가며 호출 범위를 좁히면 클래스를 로드하는 구간을 추려서 볼 수 있습니다.
인터프리터의 경우와 마찬가지로 tui를 통해 콜 그래프를 보면
좀더 상세한 정보를 한 눈에 확인 할 수 있습니다.
트레이싱을 통해 생성한 함수와 호출 흐름이 있으므로,
그 순서에 맞춰 소스코드를 분석 해들어가면 됩니다.
네비가 따로없죠.
클래스 관련 부분을 분석하기 어려웠던 이유가 여기서 나옵니다.
클래스는 자바 기반 가상머신의 핵심 중에 핵심이기 때문에 클래스를 다양한 방법으로 자주 로드합니다.
또한 클래스는 상속이라는 특징이 있어 하나의 클래스를 생성하기 위해 몇 번이고 상위 클래스들이
로드됐는지 확인하고 로드되지 않았다면 로드하는 작업을 반복하게 됩니다.
하지만 Hello world를 출력하는 과정를 트레이싱 한 결과를 통해서는
시작과 끝을 명확하게 알 수 있었기에 호출 흐름을 정리하는데 큰 도움이 됐습니다.
클래스가 로드되고 클래스의 메소드에 존재하는 바이트코드들이
인터프리터의 MterpInvoke로 시작하는 함수들과 매핑되어 실행되는 동작 원리를
파악하고 나자 나머지는 어렵지 않게 진행되었고, 소기의 목적을 달성할 수 있었습니다.
참고로 좌측이 바이트코드를 사람이 읽을 수 있게 텍스트화 한 스몰리 코드이고
우측이 DEX의 로드가 끝난 이후에 Main Class의 init을 호출하여 Main Class의
인스턴스를 생성하고 해당 인스턴스에서 public static void main 함수를 찾아
직접적으로 호출함으로서 실행 파일의 시작지점부터 실행을 하게 만들어주는
코드입니다. 혹시 궁금하신 분이 있으실까 해서 덧붙여 봤습니다.
이번에는 Gcc를 트레이싱 했던 경험을 공유해보려고 합니다.
Gcc의 경우는 특정 문제의 원인을 찾았던 경험인데요.
C로 코딩을 하던 중에 목적한 대로 코드가 동작하지 않는 경우에 직면해서
문제가 발생하는 부분만을 추려낸 소스코드를 작성한 뒤에
Gcc와 Clang 각각 컴파일하여 실행해봤습니다.
Gcc와 clang의 결과가 다르게 출력이 되어서 이상하다는 생각이 들었습니다.
Gcc와 clang으로 컴파일한 바이너리를 각각 디스어셈블 해봤는데요.
큰 차이가 있습니다.
코드가 의도한 바는 get_msg 함수의 리턴 값을 char *p에 저장하는 것인데
Get_msg함수의 리턴 값이 p로 전달되기 직전에 갑자기 0으로 바꿔버리는겁니다.
0이 p에 전달되니 puts는 NULL을 받은 것과 마찬가지고
segment fault가 날 수 밖에 없죠.
그냥 코드를 변경할까 하는 생각이 들어,
반환하는 문자열을 캐릭터 배열이 아닌 캐릭터 포인터로 변경해봤습니다.
당연히 출력은 정상적으로 나오겠죠.
하지만 제가 이 코드에서 의도했던 바는 특정 문자열을 가져다가 수정하려는 것인데
캐릭터 포인터로 변경하면 수정을 할 수가 없습니다.
보시는 바와 마찬가지로. 헬로 월드가 아니라 할로 월드를 출력하려고 하면 문제가 발생합니다.
문제의 원인은 컴파일러가 문자열을 저장하는 공간이
쓰기 불가 영역이기 때문입니다.
변경이 안되는거죠.
그럼 동적 할당으로 바꿀까? 했지만
동적할당으로 바꾸면 문자를 수정할 수 있는 대신
매번 해제를 해줘야 하죠.
그래서 한번 문제가 뭔지나 알아보자고 생각을 했습니다.
예제를 컴파일해보면, gcc가 워닝 메시지로 함수가 지역 변수 주소를 리턴한다고
알려줍니다
Gcc 소스코드를 다운받아 검색을 하면 출력 지점을 찾을 수 있습니다.
get_msg 함수의 반환 값이 0이 되버리는 이유는
함수의 리턴 값이 주소 표현인데 ,
외부 변수나 정적 변수가 아니고,
로컬 변수면 0으로 바꿔버리고 있었기 때문입니다.
아까 디스어셈블했던, 리턴 값을 0으로 만드는 어셈블리 코드는
바로 여기서 나온거죠!
일반적인 함수의 로컬 변수라면 모르겠지만,
인라인 함수는 인라인 되는 함수 스택에 할당되니 굳이 0으로 바꿔줘야 할 필요가 있을까?
사실상 리턴을 하는 것도 아니고 인라인 되는 함수 내에서 동작하는 코드와 동일한데 말이죠?
인라인 함수인 경우에는 여기를 거르면 될지 않을까? 하는 생각도 들구요.
혹시나 해서 디버거로 확인해보니 get_msg 함수의 inline_flag는 1로 셋이 되어 있었습니다.
그래서 한번 inline 함수인 경우에는 로컬 스택을 반환해도
허용해주게 바꿔봤습니다.
동작을 하긴 하는데…
추가적으로 최적화 옵션을 줬을 때
아무 출력도 되지 않는 문제가 발생했습니다.
이건 또 왜이러는거지?
다시 디스어셈블 했습니다.
마지막에는 갑자기 뭐가 많이 없어졌는데요?
처음 발생한 문제인 리턴 값을 무조건 0 으로 바꾸던 문제는
최적화 옵션을 안주면 발생하지 않는 것은 확인할 수 있습니다.
지금까지의 변화를 C로 한번 표현해봤습니다.
최적화 옵션을 주게 되면, C로 표현한 것처럼
헬로 월드라는 문자열이 증발 되버리는 겁니다.
왜 그런지 추적하기에는 단순 트레이싱만으론 중과부족이었구요.
Gcc에서는 최적화 과정의 변화를 dump해서 볼 수 있는 옵션을 지원하는데요.
이를 이용하면 트레이싱 할 수 있었습니다.
덤프 옵션으로 생성된 파일 중에
40t dse1에서 문자열을 지웠다는 메시지를 출력한걸 확인한 뒤에
어디서 문자열을 지우는지 찾기 위해 트레이싱을 시도했습니다.
400만 줄 정도의 기록이 남지만,
이미 덤프로 Deleted dead store라는 키워드를 확보했으니
이걸로 검색해보면 로그를 남기는 함수를 쉽게 찾을 수 있었습니다.
Delete_dead_assignment 가 그 함수네요.
좀더 편하게 찾기 위해 소스 브라우징이 되는 tui를 다시 띄웠습니다.
Delete_dead_assignment를 검색해서 위치를 찾은 다음에
U키를 눌러서 소스코드가 표시되는 함수까지 찾아갑니다.
Dse_dom_Walker::dse_optimize_stmt 함수가 위치한 소스코드가 표시되는게 보입니다.
Tree-ssa-dse.c 소스의 최상단에는 여기서 하는 작업에 대해 상세한 설명이 주석으로 적혀 있습니다.
다 이해는 못하더라도,
여기서 참조되지 않는 메모리를 삭제한다는 것은 알겠더라구요.
최적화 할 때 문자열을 삭제하는 것인지를 확인해보기 위해
최적화 한 경우와 아닌 경우를 비교해보려고 합니다.
비교하기 위해 한쪽 str에만 메모리 최적화 방지 키워드인 volatile을 지정했습니다.
최적화 1 옵션을 주고 dump를 출력하도록 옵션을 주고 컴파일 합니다.
생각했던 것처럼 volatile 키워드를 줬을 경우 “Hello world”가 지워지지 않는것을
확인했습니다.
그러면 트레이싱한 결과를 uftrace를 통해 그래프로 시각화를 하면서 ,
동시에 “Hello world”를 삭제하는 함수 delete_dead_assignment의 부모 함수,
“dse_dom_walker::dse_optimize_stmt”로 필터링 옵션을 줘서
이 구간이 volatile을 준 코드를 컴파일 한 경우와 그렇지 않은 경우에
호출 흐름이 서로 다른지 비교해봅시다. 비교는 winmerge로 했습니다. ,
눈으로 확인할 수 있는 것처럼 volatile을 주지 않으면 최적화가 이뤄지는 과정에서
몇 개의 함수를 추가로 호출한 뒤에 delete_dead_assignment를 호출하여
“Hello world”를 삭제한다는 확신을 할 수 있습니다.
이런 비교 결과를 토대로, “Hello world”라는 문자열은 컴파일러가,
최적화 과정 중에 지워버린다는 것을 확인 할 수 있었습니다.
그렇다면 “Hello world”가 최적화가 되지 않도록 하면 되지 않을까요?
소스코드를 좀더 수정하여,
“Hello world”라는 지역변수 tree를 반환할 때
함수가 Inline으로 선언된 경우는 0으로 바뀌지 않으면서
동시에 최적화 하지 않도록 volatile_flag를 1로 셋 하도록 바꿔봤습니다.
예상했던 대로 이제 최적화를 해도 “Hello world”가 제대로 출력됩니다.
최적화 1단계와 2단계 모두 정상 동작하네요!
지금까지 제가 Tracing을 했던 경험을 소개 드렸는데요.
이 과정에서 느낀 바를 공유하고자 하고 싶어서
앞선 사례들을 먼저 장황하게 설명한 겁니다.
물론 트레이싱의 도움도 받았지만,
가장 크게 도움을 받은 것은 보시는 Gcc에서 텍스트로 출력해주는 덤프 정보였습니다.
텍스트로 출력해준다는 것은 당장 gcc에 대한 지식이 전무한 상황이었던
제가 Gcc에서 이뤄지는 작업의 각 단계에 대한 동작과,
원리의 이해가 없이도 전과 후에 대한 비교만으로 문제가 발현되는 지점을 찾는데
큰 도움이 됐습니다.
만약에 이런 정보가 없이,
보시는 것과 같은 본래 GIMPLE 구조체가 저장하는 데이터를 가지고
문제를 추적하려고 했다면 훨씬 많은 시간과 도전이 필요했을 거라는 것은
분명하다는 거죠.
원하는 아웃풋을 얻기 위해 GCC에 두 줄의 코드를 추가하였는데
위의 작업을 진행하며 GCC에 대한 몇 가지 정보도 함께 습득할 수
있었습니다.
대략 4일 정도의 작업으로는 GCC에서 문제를 트레이싱 하면서 느낀 것은
안드로이드를 할 때와 같은 시원함을 느낄 수 없었다는 것인데요.
그래서 왜인지를 생각해보니,
안드로이드 가상머신을 분석하기 전에 이미
안드로이드 플랫폼에서,
만들어진 앱을 분석하거나 솔루션을 만들거나 하면서
부분적인 정보를 습득하고 있었던 것 같습니다.
그래서 안드로이드 가상머신을 tracing 할 때는
단순히 분석해야 할 양을 줄여주거나, 분석 지점을 찾는 것을 넘어서,
지식이 부분적이라 유기적으로 연결되지 못한 상태가,
유기적인 시스템 전체 그림을 그릴 수 있도록
Tracing이 도움을 줬던 겁니다.
그래서 어느 순간 코드가 이해되기 시작하는 시원함을 느낄 수 있었던 겁니다.
오픈소스의 접근성을 올려주는 방법을 찾고 싶기 때문입니다.
그래서 그 중 하나라고 생각하는 트레이싱을 도와주는 도구인
Uftrace에 기여하고 있습니다.
오픈소스는 오픈되어 있어, 다수의 참여로 꾸준히 성장하는데,
이런 점이 역설적으로 오픈소스로 가는 장벽이 되버립니다.
안드로이드에서의 경우처럼 부분적 정보들을
트레이싱 데이터를 가공하는 과정에서 함께 볼 수 있다면
오픈소스의 접근성을 올릴 수 있지 않겠어요?
트레이싱 데이터를 가공할 때
함수의 설명에 대한 정보를 함께 보여주면
함수의 역할에 대해 트레이싱 결과만으로도 이해할 수 있겠죠?
Gcc에서 Gimple을 dump해서 보여주는 것처럼
트레이싱 결과를 가공할 때 복합 데이터 타입을
Text로 치환된 가독성이 높은 형태로 가공해서
보여주는 접근성을 높일 수 있습니다.
이런 생각을 하는 것도 사실은 오픈소스에 대한 접근성을 높여,
저 자신이 오픈소스에 대한 분석을 효율적으로 해보고 싶기 때문입니다.