중단점으로 잘못된 접근 추적하기
130분 안팎
학습 목표
LLDB 중단점·변수 관찰과 주소 검사기로 버퍼 초과를 추적합니다.
개념
17번째 쓰기를 접근 전에 붙잡습니다
16칸의 센서 버퍼에서 길이가 16일 때 다음 기록을 samples[16]에 쓰면 마지막 원소 뒤를 접근합니다. 정상 인덱스는 0부터 15입니다. 버퍼를 포함한 큰 구조체 내부에는 뒤의 length 필드가 있어 잘못된 쓰기가 그 값을 훼손할 수도 있습니다. 주소 검사기가 항상 이 내부 경계를 찾아줄 것이라 기대하지 말고 논리 경계 검사와 필드 보존 테스트를 함께 사용합니다. 이 레슨은 잘못된 접근 직전 상태를 관찰하고 같은 경계가 수정 후 거부되는 근거를 남깁니다.
미션 starter에는 앞 모듈의 main.c, sensor.c, sensor.h와 기존 검사 코드가 그대로 들어 있습니다. recorder는 이전 CLI로 유지되고 새 record-buffer가 구조체 버퍼를 사용합니다. 이전 회귀를 깨뜨리면서 기능을 늘리지 않도록 두 실행 경로를 구별합니다. Buffer의 samples[16]은 저장 공간, length는 유효 기록 수입니다. Buffer가 공간을 소유하고 push와 get은 Sample 값을 복사합니다. 내부 원소 주소를 반환하는 API는 제공하지 않습니다.
buffer_init은 길이를 0으로 초기화합니다. buffer_push는 NULL, length >= SAMPLE_CAPACITY, sample.raw > 4095를 먼저 거부합니다. 성공하면 현재 length 위치에 복사하고 길이를 하나 늘립니다. 실패하면 버퍼를 변경하지 않습니다. buffer_get은 NULL 입력과 출력, 손상된 length, index >= length를 거부하고 성공할 때만 out에 한 건을 복사합니다. capacity 이내라도 length 밖의 칸은 기록으로 읽을 수 없습니다.
재현 입력을 작게 고정합니다
debug_probe.c는 값 0부터 16까지 17건을 차례로 추가합니다. 그때 timestamp_ms는 0부터 1600까지 100씩 증가합니다. 앞의 16건은 성공하고 마지막 시도는 실패하며 length는 16을 유지해야 합니다. 값을 여러 가지로 무작위 변경하기 전에 이 입력을 고정하면 조건식과 상태 전이를 직접 대응시킬 수 있습니다. 출력 index는 추가 시도 번호이고 저장된 건수 length와 의미가 다릅니다.
새 CLI에서는 정수 토큰 순서마다 100ms를 소비하는 가상 시각을 사용합니다. INVALID도 한 칸의 시간 경과를 나타내므로 0 -1 4095를 넣으면 정상 기록의 시각은 0과 200입니다. 가득 찬 뒤의 유효 토큰은 FULL을 출력하고 이전 16건을 덮어쓰지 않습니다. 빈 입력은 BUFFER 0으로 끝납니다. 실제 센서 시각을 읽는 코드가 아니라 PC의 결정적 입력 모델이라는 범위를 README에 명시합니다.
0·16·17건으로 비어 있음, 정확히 가득 참, 초과를 각각 검사합니다. NULL, 잘못된 raw, 손상된 length도 별도 함수 검사에 포함합니다. push 뒤 원본 Sample을 수정해도 보관값이 바뀌지 않아야 하고 get 뒤 out을 수정해도 내부 기록이 변하면 안 됩니다. 이런 테스트는 복사 정책을 확인합니다. int 파서의 형식 오류와 범위 오류는 앞 모듈 회귀로 계속 검사합니다.
LLDB로 원인과 방어 조건을 관찰합니다
-g -O0으로 빌드한 debug-probe를 lldb ./debug-probe로 엽니다. breakpoint set -n buffer_push -c 'buffer->length == 16'으로 17번째 요청에서만 멈추게 합니다. run 후 frame variable buffer sample과 expression buffer->length로 길이를 보고 bt로 호출자를 확인합니다. 이 시점은 함수 입구이므로 접근이 일어나기 전의 관찰입니다. 조건을 만족한 중단점과 실제 잘못된 쓰기가 발생한 진단을 혼동하지 않습니다.
정상 구현을 관찰하면 length가 16인 진입에서 return 0으로 가고 내부 기록은 유지됩니다. next로 검사 줄을 따라가거나 continue로 실행을 마칩니다. 정상 종료 뒤 quit로 디버거를 나옵니다. 실행 중 대상을 종료하는 명령은 사용하지 않습니다. 시작이 권한 정책에 막히면 오류를 관찰 기록에 남기고 해당 명령의 출력 칸은 비웁니다. 권한 실패를 코드 수정 성공이나 디버거 값 관찰로 바꿔 쓰지 않습니다.
잘못된 인덱스를 직접 읽어 값을 확인하려 하지 않습니다. 길이와 인덱스, 접근 예정 위치를 먼저 관찰합니다. DEBUG.md에는 failure_location, index, length, lifetime, command, observation을 기록합니다. failure_location은 예상 위험 위치 또는 실제 진단이 가리킨 파일·줄이며 어느 종류인지 적습니다. lifetime에는 Buffer가 main의 블록 안에서 살아 있고 호출 중 주소가 빌려진다는 근거를 적습니다. 값이 없는 형식만 채우면 코드 리뷰에서 증거로 인정하기 어렵습니다.
LLDB가 변수 정보를 못 보여 주면 -g 여부와 관찰한 프레임을 확인합니다. optimized out은 최적화된 빌드와 관련될 수 있어 -O0 파일로 다시 재현합니다. 중단점이 unresolved 상태라면 함수 이름과 실행 파일을 확인합니다. 이번 도구 명령에는 환경별 프로세스 번호와 주소가 생길 수 있으므로 그대로 기록하되 다른 사람에게 같은 숫자가 나올 것을 요구하지 않습니다. 비교할 핵심은 length와 실패 반환 경로입니다.
주소 검사기를 별도 프로세스에서 실행합니다
AddressSanitizer는 실행 중 잘못된 메모리 접근을 보고하는 도구입니다. -fsanitize=address로 빌드하며 UndefinedBehaviorSanitizer는 -fsanitize=undefined로 함께 켭니다. 배포용 실행 파일을 대신하는 것이 아니라 테스트용 실행 파일을 만듭니다. 접근 종류와 문제 스택, 할당 위치를 읽어 원인을 좁힙니다. 실행하지 않은 경로나 구조체 내부의 모든 논리 경계를 증명하는 도구는 아닙니다.
overflow_probe.c는 동적으로 할당한 unsigned short 16칸의 바로 뒤에 쓰는 고의 오류를 담습니다. 실제 기록기의 데이터와 상태를 손상시키지 않도록 독립 자식 프로세스에서만 실행합니다. python3 asan_check.py는 해당 자식이 실패하고 heap-buffer-overflow 진단을 냈는지 확인합니다. 문제 프로그램의 실패와 검사 스크립트의 성공은 구별합니다. 스크립트 성공은 오류 재현에 성공했다는 뜻이지 고의 오류 코드가 안전하다는 뜻이 아닙니다.
make test는 정상 test_buffer.c를 UBSan으로 다시 빌드해 별도 프로세스로 실행합니다. ASan 검사는 환경 밖의 bash check.sh에 분리합니다. 수정본에는 진단이 없고 종료 코드 0이어야 합니다. 도구가 시작 단계에서 멈추거나 환경 오류를 내면 논리 테스트 성공과 도구 검증 미완료를 따로 기록합니다. 실행 결과를 보지 못한 명령에는 예상 출력으로 성공을 만들어 붙이지 않습니다. 환경 밖 bash check.sh는 고의 ASan 진단과 정상 버퍼 무진단, LLDB 관찰값 및 자연 종료를 확인합니다. 이 맥에서 ASan 시작이 정체되고 LLDB 실행이 거부되어 verify=external로 표시합니다. 검증기의 PENDING은 완료가 아니며 실제 check.sh 결과를 후속으로 확인합니다.
확장 미션의 제출 기준
미션을 완료하면 기존 7개 범위 검사와 10개 CLI 사례, 새 버퍼 함수 검사, 8개 추가 CLI 사례가 함께 통과해야 합니다. DEBUG.md의 필수 항목 검사는 형식만 확인하고 실제 관찰값은 사람이 리뷰합니다. make test의 통과 문장 하나로 LLDB 관찰까지 자동 증명했다고 말하지 않습니다. 테스트 결과와 명령 기록을 README의 소유권·길이·가득 참 정책 설명과 함께 제출합니다.
버퍼가 가득 찼다고 길이를 0으로 돌리면 지금의 보존 계약을 깨뜨립니다. 링 버퍼로 바꾸려면 덮어쓰기 순서와 손실 정책을 새로 정의해야 하므로 이번 미션에서 임의로 도입하지 않습니다. 다음 HAL 모듈이 이 버퍼와 입력 모델을 확장할 수 있도록 buffer.h의 함수 선언과 Sample 필드 이름을 유지합니다. 더 읽기에서 경고·디버거·검사기 각각의 역할을 보충하고 본인의 경계 실패 근거를 면접 답변으로 설명합니다.
따라하기
경계 재현 프로그램 만들기
starter의 buffer.c를 구현한 뒤 생성합니다. 컴파일은 성공 시 출력하지 않습니다.
make -s debug-probe17번째 요청 비교
첫 16건의 수락과 마지막 거부 및 길이 보존을 비교합니다.
./debug-probe실행 결과
index=0 accepted=1 length=1 index=1 accepted=1 length=2 index=2 accepted=1 length=3 index=3 accepted=1 length=4 index=4 accepted=1 length=5 index=5 accepted=1 length=6 index=6 accepted=1 length=7 index=7 accepted=1 length=8 index=8 accepted=1 length=9 index=9 accepted=1 length=10 index=10 accepted=1 length=11 index=11 accepted=1 length=12 index=12 accepted=1 length=13 index=13 accepted=1 length=14 index=14 accepted=1 length=15 index=15 accepted=1 length=16 index=16 accepted=0 length=16
중단점에서 수명과 길이 읽기
LLDB 명령을 한 줄씩 입력합니다. 실행 정책 때문에 run이 거부되면 실제 오류와 미완료 상태를 DEBUG.md에 적습니다. 실행 가능하면 length 16과 sample 시각 1600을 관찰하고 continue로 자연 종료 후 quit합니다.
lldb ./debug-probe
breakpoint set -n buffer_push -c 'buffer->length == 16'
run
frame variable buffer sample
bt
continue
quit누적 CLI 실행
record-buffer는 새 CLI이고 recorder는 앞 모듈 회귀용입니다. 가상 시각이 INVALID에도 증가하는지 봅니다.
make -s record-buffer
printf '0 -1 4095\n' | ./record-buffer실행 결과
STORED 0 0 INVALID STORED 200 4095 BUFFER 2 RECORD 0 0 RECORD 200 4095
전체 회귀와 도구 검사
먼저 make test로 기존 회귀·새 경계·UBSan 검사를 확인합니다. ASan과 LLDB 실행을 지원하는 환경에서는 bash check.sh로 고의 오류 진단·수정본 무진단·중단점의 실제 값과 자연 종료까지 확인합니다. 샌드박스에서는 뒤 명령을 실행하지 않으며 환경 밖 결과는 미확인입니다.
make -s test실행 결과
range: 7 cases, 0 failures link: missing definition rejected cli: 10 cases passed PASS pc-workbench buffer: boundary, null, copy, corrupt-length; 0 failures memory-cli: 8 cases passed debug record: fields present (values require review) UBSan: corrected buffer passed PASS memory-records
환경 밖 전체 검증
이 맥에서는 ASan 초기화와 LLDB 실행 정책으로 완료하지 못한 명령입니다. check.sh 성공 여부와 DEBUG-actual.txt의 실제 관찰을 확인합니다. 출력 칸은 외부 검증 전이므로 비워 둡니다.
bash check.sh확인 문제
실습
앞 모듈을 포함한 누적 미션입니다. buffer.c의 push/get을 구현하고 DEBUG.md의 여섯 항목에 명령과 관찰값을 씁니다. 16칸 초과는 거부하고 과거 기록을 보존합니다. make test로 이전 회귀와 새 경계 검사를 통과시키며 샌드박스 밖에서 bash check.sh로 ASan과 실제 LLDB 관찰을 추가 확인합니다. 환경 밖 검증은 PENDING이며 make test의 성공과 구별합니다.
실행 명령
bash check.sh
기대 결과
make test: PASS memory-records. 외부 check.sh: ASan 진단·수정본 무진단·LLDB 관찰 및 자연 종료 확인.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 배열과 포인터를 사용할 때 메모리 오류를 확인하는 방법을 설명해 주시면 됩니다.