C 성능·Makefile·lldb: 캐시 순서 실측과 크래시 추적

C 강좌 6강. 행/열 순회 54배 차이, 구조체 배열 대 배열 구조체, 헤더 의존성을 챙기는 Makefile과 함정 세 가지, lldb의 bt·frame variable로 크래시 원인 찾기까지 실제 실행.

시리즈: C 강좌 6/6 (고급). C를 올바르게 쓸 줄 알고, 이제 빠르게 만들고, 여러 파일 프로젝트로 빌드하고, 남의 컴퓨터에서 죽었을 때 디버깅해야 하는 분을 위한 글입니다. 다 읽고 나면 같은 일을 하는 두 루프 중 어느 쪽이 빠를지 예측하고 재 볼 수 있고, 바뀐 파일만 정확히 다시 빌드하는 Makefile을 쓰고, lldb로 크래시의 호출 스택과 지역 변수를 꺼낼 수 있습니다.

학교에서 C를 Visual Studio로 배운 분은 F5 한 번에 빌드와 디버거가 같이 돌았으니, 회사에서 터미널과 Makefile, lldb(또는 gdb)를 처음 만나면 당황하기 쉽습니다. 게다가 실무 C에는 세 가지 일상적인 문제가 있습니다. 같은 합을 구하는 두 루프가 50배 차이가 나고, 프로젝트가 오래된 오브젝트 파일로 "성공적으로" 빌드되어 엉터리 값을 찍고, 크래시 보고가 Segmentation fault: 11 한 줄뿐입니다. 시리즈 마지막 강의에서 셋 다 직접 돌린 프로그램으로 다룹니다. 숫자는 실측, 에러 메시지는 복사, lldb 세션도 실제입니다.

실행 환경: Apple clang 21.0.0, GNU Make 3.81(macOS 기본), lldb-2103, Apple Silicon Mac(M1 Pro), macOS. 시간은 clock_gettime(CLOCK_MONOTONIC)으로 한 실행 안에서 5회 중 최솟값이고, 전체 실행을 네 번 반복해 마지막 값을 인용했습니다. 경로는 ~/demo/…로 줄였습니다. 전체 학습 순서는 프로그래밍 학습 로드맵에 있습니다.

메모리 순서가 중요한 이유: 캐시 라인 한 문단 요약

CPU 코어는 RAM에서 바이트 하나씩 읽지 않습니다. 캐시 라인(대부분의 x86-64는 64바이트, 이 M1 Pro는 sysctl에서 hw.cachelinesize: 128) 단위로 작고 빠른 캐시에 가져오고, 앞으로 걸어가는 패턴을 보면 다음 라인을 미리 가져옵니다(프리페치). 그래서 주소 순서대로 읽으면 첫 접근 뒤로는 거의 공짜이고, 여기저기 뛰면 매번 느린 단계까지 다녀와야 합니다. C에서는 이게 그대로 드러납니다. 2차원 배열 m[i][j]는 행 단위로 저장되므로 m[i][j]와 m[i][j+1]은 이웃이고, m[i][j]와 m[i+1][j]는 한 행만큼 떨어져 있습니다.

실측: 행 순서 대 열 순서, 54배

static int64_t sum_rows(const int *m) {            /* m[i][j], j가 안쪽: 순차 접근 */
    int64_t s = 0;
    for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) s += m[(size_t)i * N + j];
    return s;
}
static int64_t sum_cols(const int *m) {            /* m[i][j], i가 안쪽: 16KiB씩 점프 */
    int64_t s = 0;
    for (int j = 0; j < N; j++) for (int i = 0; i < N; i++) s += m[(size_t)i * N + j];
    return s;
}

N = 4096이면 행렬은 int 1,680만 개, 64MiB로 칩의 어떤 캐시보다 큽니다. 두 함수는 같은 수를 다른 순서로 더합니다.

rows (i outer)             1.13 ms  (result 8583959407)
columns (j outer)         61.58 ms  (result 8583959407)

결과는 같고 시간은 54배. 행 루프는 64MiB를 대략 59GB/s로 흘려 읽고, clang이 벡터화(명령 하나로 int 여러 개를 더함)까지 했습니다. 열 루프는 한 번에 4096 × 4 = 16KiB씩 건너뛰니 캐시 라인 하나에서 쓸모 있는 int는 하나뿐이고, 이웃이 필요해지기 전에 라인이 밀려납니다. 네 번의 전체 실행에서 행은 1.13~1.48ms, 열은 60.5~63.0ms 사이였습니다.

흔한 실수: 수식에 "각 열 j에 대해, 각 행 i에 대해"라고 쓰여 있는 걸 그대로 이중 루프로 옮기기. 열 우선으로 저장하는 Fortran이나 MATLAB 코드를 옮길 때 특히 자주 생깁니다. 실제로 일어나는 일: 결과는 맞는데 조용히 50배 느립니다. 해결: 마지막 인덱스를 가장 안쪽 루프로. 열 순회가 핫 패스라면 데이터를 한 번 전치해 둡니다.

구조체 배열(AoS) 대 배열 구조체(SoA)

레코드에도 같은 생각이 적용됩니다. 입자 1,000만 개를 두 가지로 저장합니다.

struct Particle { double x, y, z, vx, vy, vz, mass; int64_t id; };    /* 64바이트 */
struct Particles { double *x, *y, *z, *vx, *vy, *vz, *mass; int64_t *id; };

첫째는 구조체의 배열(Array of Structs, AoS)로 p[i].x 뒤에 p[i].y, 그다음 입자 i의 나머지가 이어집니다. 둘째는 배열의 구조체(Struct of Arrays, SoA)로 x 값들이 연속으로 붙어 있습니다. x만 합산하면:

AoS sum of x              10.87 ms  (result 4995000000)
SoA sum of x               9.35 ms  (result 4995000000)
AoS sum of x, 4 acc       10.67 ms  (result 4995000000)
SoA sum of x, 4 acc        2.35 ms  (result 4995000000)

처음 두 줄이 솔직한 의외의 결과입니다. SoA가 겨우 14% 빨랐습니다. 단순한 두 루프 모두 메모리가 아닌 다른 것에 막혀 있었습니다. double 합 s += …는 사슬이라 덧셈마다 앞 덧셈이 끝나기를 기다려야 하고(부동소수점 덧셈은 몇 사이클 걸림), 부동소수점 덧셈은 결합 법칙이 성립하지 않아 순서를 바꾸면 결과가 달라질 수 있으니 컴파일러가 마음대로 재배치하지 못합니다. 그래서 두 루프 모두 "덧셈 지연 시간당 덧셈 하나" 속도로 돌았습니다.

사슬을 독립된 합 네 개(s0 += x[i]; s1 += x[i+1]; …)로 쪼개면 그 제한이 풀리고 이제 메모리가 속도를 정합니다. SoA는 2.35ms로 떨어졌고, AoS는 쓰는 8바이트마다 64바이트짜리 입자 전체를 캐시로 끌고 오기 때문에(한 바퀴에 640MB 대 80MB) 10.67ms에 머물렀습니다. 데이터 배치는 루프가 메모리를 기다릴 만큼 빨라진 다음에야 효과가 난다. 코드베이스를 SoA로 뜯어고치기 전에 먼저 재 보세요. (여기서는 값이 정수라 네 갈래 합도 결과가 완전히 같습니다. 임의의 double이면 마지막 비트가 다를 수 있습니다.)

밀리초 막대그래프. 행렬 행 순서: 1.13, 열 순서: 61.58(약 54배 느림). 구조체 배열 x 합계: 10.87, 배열 구조체: 9.35. 누산기 4개로 구조체 배열: 10.67, 배열 구조체: 2.35. 덧셈 의존 사슬을 나눈 뒤에야 배열 구조체가 크게 빨라졌다.
같은 합계, 다른 메모리 순서. 4096x4096 int 행렬(64 MiB)과 1,000만 개의 64바이트 입자. Apple clang 21 -O2, Apple Silicon Mac (M1 Pro), 5회 중 최솟값, 고정 시드.

측정 프로그램 전체(cache.c, 파일 하나)는 영어판에 있습니다. 전체 할당이 약 1.3GB이니 메모리가 작은 기계에서는 P를 줄이세요.

바뀐 파일만 다시 빌드하는 Makefile

.c 파일이 여러 개가 되면 매번 손으로 전부 컴파일하는 건 느리고 실수하기 쉽습니다. make는 Makefile의 규칙("타깃: 선행 조건" 다음 줄에 타깃을 만드는 명령, 즉 레시피)을 읽고, 선행 조건이 더 새로울 때만 타깃을 다시 만듭니다. 파일 세 개짜리 프로젝트(main.c, stats.c, parse.c, 그리고 stats.h):

CC      = clang
CFLAGS  = -std=c17 -Wall -Wextra -O2 -g -MMD -MP
OBJS    = main.o stats.o parse.o

app: $(OBJS)
    $(CC) $(CFLAGS) -o $@ $(OBJS)

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

-include $(OBJS:.o=.d)

.PHONY: clean
clean:
    rm -f app $(OBJS) $(OBJS:.o=.d)

$@는 타깃, $<는 첫 선행 조건, %.o: %.c는 모든 오브젝트에 대한 패턴 규칙입니다. 핵심은 -MMD -MP입니다. clang이 컴파일하면서 main.c가 포함한 헤더 목록을 main.d에 따로 써 주고, -include가 그 목록을 Makefile로 끌어옵니다. 만들어진 파일:

main.o: \
  …/MacOSX.sdk/SDKSettings.json \
  main.c stats.h
…/MacOSX.sdk/SDKSettings.json:
stats.h:

(SDK의 SDKSettings.json이 의존성에 들어가는 건 Apple clang만의 특징입니다. Xcode를 업데이트하면 다시 빌드된다는 뜻이고, 그게 맞는 동작입니다.) 첫 빌드, 아무것도 안 바뀐 빌드, 파일 하나 수정 후 빌드:

$ make
clang -std=c17 -Wall -Wextra -O2 -g -MMD -MP -c main.c -o main.o
clang -std=c17 -Wall -Wextra -O2 -g -MMD -MP -c stats.c -o stats.o
clang -std=c17 -Wall -Wextra -O2 -g -MMD -MP -c parse.c -o parse.o
clang -std=c17 -Wall -Wextra -O2 -g -MMD -MP -o app main.o stats.o parse.o
$ ./app
count=3 mean=3.000
$ make
make: `app' is up to date.
$ touch parse.c && make
clang -std=c17 -Wall -Wextra -O2 -g -MMD -MP -c parse.c -o parse.o
clang -std=c17 -Wall -Wextra -O2 -g -MMD -MP -o app main.o stats.o parse.o

parse.o와 링크만 다시 돌았습니다. make -j4는 서로 독립인 컴파일을 병렬로 돌립니다.

Makefile 함정 세 가지, 직접 재현

함정 1: TAB 대신 공백. 레시피 줄은 진짜 TAB으로 시작해야 합니다. 편집기가 공백을 넣는 경우가 많습니다. GNU Make 3.81의 메시지:

Makefile.spaces:6: *** missing separator.  Stop.

6번째 줄이 첫 레시피 줄입니다. cat -t Makefile로 보면 진짜 탭은 ^I로 보이고, 공백이 보이면 그게 버그입니다. VS Code 같은 편집기에서 Makefile은 탭을 유지하도록 설정하세요.

함정 2: 헤더 의존성 없음. 아무것도 실패하지 않아서 가장 위험합니다. 같은 Makefile에서 -MMD -MP와 -include를 뺀 버전으로 빌드한 뒤, stats.h의 구조체 맨 앞에 필드를 하나 추가합니다.

struct stats { const char *label; long count; double sum; };   /* 원래: { long count; double sum; } */
$ make -f Makefile.naive
make: `app' is up to date.

make는 stats.h가 중요한지 모르니 아무것도 다시 빌드하지 않습니다. 다음으로 할 법한 일, main.c를 수정합니다(여기서는 touch):

$ make -f Makefile.naive
clang -std=c17 -Wall -Wextra -O2 -g -c main.c -o main.o
clang -std=c17 -Wall -Wextra -O2 -g -o app main.o stats.o parse.o
$ ./app
count=4621256167635550208 mean=3.000
exit=0

이제 main.o는 새 배치, stats.o는 옛 배치를 씁니다. stats_add(옛 배치)는 count를 오프셋 0에, sum을 오프셋 8에 썼고, main(새 배치)은 count를 오프셋 8에서 읽었습니다. 거기엔 double 9.0(1.5 + 2.5 + 5.0)의 비트가 있습니다. 0x4022000000000000 = 4621256167635550208. 경고도 크래시도 없이 exit 0. 의존성을 챙기는 Makefile로 make clean && make 하자 다시 count=3 mean=3.000. 메모리 오염과 똑같아 보이는 빌드 시스템 버그이고, "클린 빌드하니까 버그가 사라졌다"가 바로 이걸 의심할 때입니다.

함정 3: 같은 초 안의 수정. 빌드 직후 곧바로 parse.c를 touch했습니다.

make: `app' is up to date.
parse.c 1794494800.980412000
parse.o 1794494800.929907233

parse.c가 parse.o보다 0.05초 새롭지만 macOS의 Make 3.81은 초 단위로 비교해서 같다고 봤습니다. 1초 뒤 touch parse.c && make는 다시 빌드했습니다. 평소 편집에서는 거의 안 걸리지만, 파일을 생성하고 곧바로 make를 부르는 스크립트나 빠른 CI 단계에서는 걸릴 수 있습니다. GNU Make는 지원하도록 빌드되면 초 미만 시각도 비교하지만, 여기 들어 있는 3.81은 그렇지 않았습니다(다른 빌드는 시험하지 않음).

lldb로 크래시 추적하기

표에서 설정을 읽는 작은 프로그램입니다. 항목 하나에 값이 없습니다.

#include <stdio.h>
#include <string.h>

struct entry { const char *key; const char *value; };

static const struct entry table[] = {
    { "host", "example.com" }, { "port", "8080" }, { "mode", NULL },  /* mode는 값이 없다 */
};

static const char *lookup(const char *key) {
    for (size_t i = 0; i < sizeof table / sizeof table[0]; i++)
        if (strcmp(table[i].key, key) == 0) return table[i].value;
    return NULL;
}

static size_t value_len(const char *key) {
    const char *v = lookup(key);
    return strlen(v);                     /* v가 NULL이면 죽는다 */
}

int main(void) {
    const char *keys[] = { "host", "port", "mode" };
    for (int i = 0; i < 3; i++)
        printf("%s -> %zu chars\n", keys[i], value_len(keys[i]));
    return 0;
}

아무 도움 없이 얻는 정보는 이게 전부입니다(파이프로 넘기면 앞 두 줄조차 안 보입니다).

bash: line 1: 77904 Segmentation fault: 11  ./crash
exit=139

종료 코드 139는 128 + 11, 즉 시그널 11번 SIGSEGV입니다. -O0 -g(디버그 정보, 최적화 없음)로 빌드하고 lldb에서 실행합니다. -b는 배치 모드, -o는 명령 실행, -k는 프로세스가 죽었을 때만 실행할 명령입니다.

clang -std=c17 -Wall -Wextra -O0 -g crash.c -o crash
lldb -b -o run -k bt -k "frame select 1" -k "frame variable" -k "frame select 2" -k "frame variable" -k kill ./crash < /dev/null
(lldb) run
host -> 11 chars
port -> 4 chars
Process 78194 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000188df6ac4 libsystem_platform.dylib`_platform_strlen + 4
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
  * frame #0: 0x0000000188df6ac4 libsystem_platform.dylib`_platform_strlen + 4
    frame #1: 0x00000001000005a0 crash`value_len(key="mode") at crash.c:18:12
    frame #2: 0x000000010000051c crash`main at crash.c:24:46
    frame #3: 0x0000000188a244e4 dyld`start + 6992
(lldb) frame select 1
frame #1: 0x00000001000005a0 crash`value_len(key="mode") at crash.c:18:12
-> 18       return strlen(v);                     /* crashes when v is NULL */
(lldb) frame variable
(const char *) key = 0x000000010000067a "mode"
(const char *) v = 0x0000000000000000
(lldb) frame select 2
(lldb) frame variable
(const char *[3]) keys = ([0] = "host", [1] = "port", [2] = "mode")
(int) i = 2

순서대로 읽습니다. EXC_BAD_ACCESS (code=1, address=0x0)은 주소 0 읽기, 즉 NULL 포인터입니다. 프레임 0은 C 라이브러리(strlen) 안이고, 버그가 거기 있는 경우는 없습니다. 프레임 1이 우리 코드입니다. key="mode"인 value_len, frame variable에 v = 0x0. 프레임 2는 루프 세 번째(i = 2)였다고 말해 줍니다. printf 하나 안 넣고 화면 한 장으로 진단 끝. (배치 모드 메모: 처음에 -k up을 쓰고 표준 입력을 열어 둔 채 실행했더니 끝나지 않아서 직접 종료해야 했습니다. < /dev/null로 입력을 닫고 -k kill로 끝내자 그 뒤로는 매번 깔끔하게 끝났습니다.)

-O2 -g로 빌드하면 같은 세션이 덜 친절합니다.

    frame #1: 0x00000001000004b0 crash2`value_len(key=<unavailable>) at crash.c:18:12 [opt] [inlined]
warning: crash2 was compiled with optimization - stepping may behave oddly; variables may not be available.
(lldb) frame variable
(const char *) key = <no location, value may have been optimized out>
(const char *) v = 0x0000000000000000

[opt] [inlined]: 함수가 main에 합쳐져 key는 어느 레지스터에도 남아 있지 않습니다. 가능하면 -O0에서 디버깅하고, 배포는 -O2 -g로 해서 사용자 크래시 보고에도 줄 번호가 남게 하세요.

조건부 브레이크포인트를 쓰면 나쁜 경우에만, 크래시 직전에 멈춥니다.

(lldb) breakpoint set -f crash.c -l 18 -c 'v == 0'
Breakpoint 1: where = crash`value_len + 28 at crash.c:18:19, address = 0x0000000100000598
(lldb) run
host -> 11 chars
port -> 4 chars
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000598 crash`value_len(key="mode") at crash.c:18:19
(lldb) frame variable
(const char *) key = 0x000000010000067a "mode"
(const char *) v = 0x0000000000000000

비교하자면 -fsanitize=address는 같은 크래시를 디버거 없이 보고서로 바꿉니다. ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 … Hint: address points to the zero page. 다음에 #1 … in value_len crash.c:18, #2 … in main crash.c:24가 나옵니다. Visual Studio 디버거의 "호출 스택" 창과 "지역" 창이 각각 bt와 frame variable에 해당한다고 보면 됩니다.

결정표: 증상별 도구

판단 도식. 출발점은 뭔가 이상하다. 질문 1: 죽는가? 예면 -O0 -g 빌드를 lldb로 실행해 bt와 frame variable. 아니오면 질문 2: 값이 틀린가? 예면 -fsanitize=address,undefined로 다시 빌드하고 첫 보고를 읽는다. make clean 후 사라지면 헤더 의존성 누락을 의심하고 -MMD -MP. 느리기만 하면 -O2, 고정 시드로 두 가지 메모리 순서를 잰다.
증상별로 먼저 쓸 도구. 디버그 빌드는 -O0 -g. 새니타이저와 lldb는 답하는 질문이 다르다.
증상 도구 볼 곳
결과는 맞는데 배열 루프가 느림 루프 순서 두 가지를 -O2로 측정 가장 안쪽 루프가 연속 주소를 걷는가
구조체 다수에서 필드 하나만 쓰는 루프가 느림 SoA와 독립 누산기를 둘 다 시도 메모리에 막혔나, 지연 시간에 막혔나
Segmentation fault: 11 / exit 139 -O0 -g 빌드로 lldb -b -o run -k bt -k "frame variable" 내 코드인 첫 프레임
크래시 없이 값이 망가짐 -fsanitize=address,undefined (3~5강) 마지막이 아니라 첫 보고
make clean 후 버그가 사라짐 헤더 의존성(-MMD -MP) .d 파일이 있고 include되는가
*** missing separator. Stop. cat -t Makefile 레시피 줄이 ^I로 시작하는가

한계와 버전 메모

  • 시간은 모두 Apple Silicon Mac(M1 Pro) 한 대, Apple clang 21.0.0 -O2, 실행 안에서 5회 최솟값, 전체 네 번 실행입니다. 절대값은 메모리 시스템에 따라 다르고, 54배라는 비율도 CPU마다 다르겠지만 방향은 같습니다.
  • 첫 절의 캐시 라인 크기는 sysctl 값과 일반 지식이고, 우리가 직접 잰 건 루프 전체 시간뿐입니다.
  • GNU Make 3.81은 2006년 버전이고 macOS가 아직 이걸 넣어 둡니다. 최신 GNU Make(4.x)에는 .RECIPEPREFIX(TAB 대신 다른 문자 사용)나 "did you mean TAB instead of 8 spaces?" 힌트 같은 기능이 있습니다. 설치하거나 실행하지 않았습니다.
  • -MMD -MP는 make가 아니라 GCC/clang의 플래그입니다. 이 맥에는 GDB가 없고, lldb 명령은 GDB의 bt, frame N, info locals에 해당합니다(실행 안 함).
  • lldb 출력은 다듬었고(멈춘 뒤의 디스어셈블 줄 삭제) 경로는 줄였습니다. PID와 주소는 우리 실행의 값이라 여러분 환경에서는 다릅니다.

연습 문제

  1. 동료의 이미지 필터가 float in[H][W]에 대해 for (x = 0; x < W; x++) for (y = 0; y < H; y++) out[y][x] = f(in[y][x]);로 돕니다. 무엇을 바꾸고, 빨라졌다는 걸 어떻게 증명할까요?
  2. -MMD 없이 Makefile.naive에 최소한 무엇을 추가하면 stats.h가 바뀔 때 오브젝트 세 개가 모두 다시 빌드될까요?

정답

  1. 마지막 인덱스 x가 가장 안쪽이 되도록 루프를 바꿉니다: for (y…) for (x…). 증명은 cache.c 같은 최솟값 측정 틀로 같은 고정 시드 입력에 대해 -O2로 두 버전의 시간을 재고, 출력(또는 그 체크섬)이 같은지 확인하는 것입니다.
  2. 한 줄 추가: $(OBJS): stats.h. Make 3.81로 확인했습니다. touch stats.h 후 make가 main.o, stats.o, parse.o를 다시 컴파일하고 링크했습니다. 단점은 이 목록을 손으로 관리해야 한다는 것이고, 그래서 -MMD -MP가 낫습니다.

C 시리즈는 여기까지입니다. 이 도구들을 실제 자료구조에 써 보려면 정렬 알고리즘 글을, 막 시작하는 동료에게 시리즈를 권하려면 1강을 보세요.


이전: 5강: 정의되지 않은 동작과 최적화 · 다음: C 이후의 길은 프로그래밍 학습 로드맵에서

자주 묻는 질문

2차원 배열을 열 방향으로 돌면 왜 그렇게 느린가요?

C는 2차원 배열을 행 단위로 저장합니다. 행을 따라가면 연속된 바이트를 읽어 캐시 라인을 통째로 쓰고 벡터화도 됩니다. 4096x4096 int 행렬에서 열을 따라가면 한 걸음에 16KiB씩 건너뛰어 거의 매번 새 캐시 라인이 필요합니다. 같은 데이터로 행 순서 1.13ms, 열 순서 61.58ms, 약 54배 차이였습니다.

'Makefile: *** missing separator. Stop.'은 무슨 뜻인가요?

레시피 줄(타깃 아래의 명령)은 반드시 TAB 문자로 시작해야 하는데 공백으로 시작했다는 뜻입니다. 보통 편집기가 탭을 공백으로 바꾼 경우입니다. macOS에 들어 있는 GNU Make 3.81은 'Makefile.spaces:6: *** missing separator. Stop.'처럼 줄 번호를 알려 줍니다. cat -t로 보면 진짜 탭은 ^I로 보입니다.

printf를 넣지 않고 C 프로그램이 어디서 죽었는지 보려면?

-g(가능하면 -O0)로 빌드해 lldb에서 실행하고, 멈추면 bt로 호출 스택을, frame variable로 선택한 프레임의 지역 변수를 봅니다. 배치 모드로는 lldb -b -o run -k bt -k 'frame variable' ./app. 실제 크래시에서 value_len(key="mode")의 v = 0x0000000000000000이 crash.c:18에서 보였습니다.

  • #C
  • #C언어
  • #성능
  • #CPU 캐시
  • #Makefile
  • #lldb
  • #디버깅
  • #고급

t.laboratory

직접 만들고 운영하는 앱·서버·서비스에서 겪은 문제와, 그 해결에 쓴 코드를 기록하는 기술 블로그입니다.

운영자 소개 · 앱

↑