Vibe coding go vrrrrrr...
이번주의 삽질
일전의 글을 먼저 읽어주셔야 이해가 가시겠습니다만, 다시한번 설명하자면 디스어셈블된 C 코드를 좀 더 읽기 좋게 바꿔주고싶다…는게 최종적인 목표 되겠습니다.
다만 이전에는 어찌저찌 LLM에게 대충 넣고 ‘해줘’ 하는 방식으로 도전했다면, 이번에는 좀 더 검증가능한 방법을 적용해보자! 싶어서 아래의 방법을 시도해보고 있습니다.
Main idea: LLM에게 직접 코드를 고치게 하지 않습니다. 대신 하고싶은 리팩토링에 대응하는 정확한 툴을 만들어서 해당 툴을 이용해 코드를 변경합니다.
다만 여기서 말하는 리팩토링은 일반적인 IDE에서 제공하는 것과는 약간 다릅니다. function의 parameter type을 변경하는 것은 일반적인 IDE에서 제공하는 기능이 아니기 때문에, 해당 기능을 다음과 같이 구현해야 합니다.
- 동작 변경 없이 함수 파라미터 타입을 변경합니다.
예를 들어 void func(int a);
함수에서 a 파리미터의 타입을 int에서 struct576*
으로 변경한다고 하면,
struct576* a_
으로 파라미터를 변경하고, 함수의 첫 줄에 int a = a_;
구문을 넣어줘서 함수의 기존 구현이 그대로 동작할
수 있도록 해 줍니다.
- 이런저런 pointer arithmetic을 해석해서 실제로 포인터 위치에서 접근하는 offset 위치를 계산해 냅니다.
int a = a_;
uint32_t* b = a_;
call(a + 32); // a1_에서 + 32바이트 offset
call(b + 8); // a1_에서 + 8 * sizeof(uint32_t) 바이트 offset = 32
C 문법상 두개의 call 모두 같은 offset을 지칭하기 때문에 alias의 타입과 해당 타입의 operator+의 작동을 세심하게 판단해줄 필요가 있습니다.
- offset 위치에 해당하는 struct 내의 field를 찾습니다.
각 field마다 차지하는 크기를 계산해서 offset -> field name 맵을 만들 수 있습니다. 한가지 주의할 점은 padding이 있는 구조체의 경우 해당 padding을 감안해서 계산해야 한다는 점입니다. 일단 제 경우엔 padding이 끼어들 여지가 없도록 field를 구성했습니다. 이를테면
struct {
uint8_t field_0;
uint8_t field_0_1; // unused
uint8_t field_0_2; // unused
uint8_t field_0_3; // unused
uint32_t field_1;
}
이와같은 식으로 unused field를 사이사이에 끼워넣으면 padding을 고려하지 않아도 됩니다.
- 2와 3을 조합해서 offset을 적절히 translate하는 툴을 만들되, 잘 안되는 부분은 적당히 하드코딩으로 때워봅니다.
어차피 예외상황은 많지 않기 때문에 하드코딩도 적당히 허용가능합니다.
claude-code logger
Claude pro 구독자는 claude-code 툴을 사용할 수 있습니다만, 해당 툴이 어떤 데이터를 외부로 보내는지 알고싶어서 claude-code-logger라는 툴을 이용했습니다.
다만 해당 툴이 pipe 모드를 제대로 지원하지 않아서, 다음과 같은 툴을 따로 만들어 대응했습니다.
{
lib,
buildNpmPackage,
fetchFromGitHub,
fetchzip,
nodejs_20,
}:
buildNpmPackage rec {
pname = "claude-code-logger";
version = "1.1.0";
nodejs = nodejs_20; # required for sandboxed Nix builds on Darwin
src = fetchzip {
url = "https://github.com/arsenyinfo/claude-code-logger/archive/refs/heads/main.zip";
hash = "sha256-NLQX6y+F8+RmD7oIXOK2bU//59KsVlvyVsWHR074O5A=";
};
npmDepsHash = "sha256-funqR2VXOzM8pkkW+Nf673D8X9Ad/f29I2DaT1ui+s4=";
postPatch = ''
cp ${./package.json} package.json
cp ${./package-lock.json} package-lock.json
'';
dontNpmBuild = true;
AUTHORIZED = "1";
# `claude-code` tries to auto-update by default, this disables that functionality.
# https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview#environment-variables
postInstall = ''
cat > $out/bin/cla << EOF
#!/usr/bin/env bash
# Generate timestamp for log file
LOG_FILE="\$HOME/logs/\$(date +%Y-%m-%d-%H-%M-%S).jsonl"
# Ensure logs directory exists
mkdir -p \$HOME/logs
# Run claude with logging and configuration
CLAUDE_API_LOG_FILE="\$LOG_FILE" \
NODE_OPTIONS='--require "$out/lib/node_modules/claude-logger/direct_capture.js"' \
DISABLE_AUTOUPDATER=1 \
claude -p --allowedTools "*" --dangerously-skip-permissions "\$@"
EOF
chmod +x $out/bin/cla
'';
passthru.updateScript = ./update.sh;
meta = {
description = "";
homepage = "https://github.com/arsenyinfo/claude-code-logger";
license = lib.licenses.mit;
mainProgram = "cla";
};
}
간단히 설명하자면 claude-code-logger를 바로 실행하는 게 아니라, 해당 툴이 사용하는 environment variable들을 적당히 비슷하게 제공하는 쉘 스크립트를 만들어서, 해당 툴이 pipe를 알아서 처리해주게 하자… 는 아이디어입니다.
Parsing C code
현재로선 treesitter의 Python binding을 사용해서 시도해보고 있습니다만, node type이 쓰기쉬운 형태는 아닙니다. 특히나 pointer가 있거나 없는 경우에 node type이 크게 달라지는 경우가 있는데 LLM에서 생성한 코드는 해당 사례를 제대로 대응하지 않아서 곤란한 경우가 많았습니다.
Prompt engineering + possible benchmarks?
대충 이런식의 프롬프트를 씁니다.
Write a python program which do the following: given the C function name,
ordinal number of the parameter and its updated type, update the C/header files
in the codebase with matching function names to have updated function signature.
The update should replace the parameter in-place with the new type and the name
suffixed with `_`, and then add a variable with orignal type and name and assign
from the new variable so that the function body keep working as before.
Suggest to use tree-sitter-c and tree-sitter packages to parse C code.
For example, I should be able to run the following code:
python update.py func1 0 uint32_t
Which will update all references of the func1, change first parameter type to uint32_t,
and add a statement which declare a variable with the old type(int) and name which
initialized with the new parameter.
ultrathink
아마도 같은 프롬프트를 claude-code 말고 gemini 등 다른 툴에 넣어보는 시도도 가능할 것 같습니다만, 시간이 많지 않아 시도해보진 않았습니다.
Meta-tool programming?
로깅 툴에 남아있는 로그를 보면 시스템 프롬프트로 ToDo 툴을 ‘아주 많이’ 쓰라고 권고하고 있고 해당 기능이 전반적인 툴 동작에 크게 유용한 것으로 보입니다. LLM이 효과적으로 작업을 수행할 수 있는 툴을 적절히 제공하는 것이 유용하다고 가정하면, 이 글에서 시도하는 툴 생성 + 적용 방식을 자동화하는 것도 가능하리라 봅니다.
위에 언급한 툴들을 자동으로 만들고 적절히 사용하도록 해 보자… 는 것이죠. 다만 claude-code는 현재 커스텀 툴을 지원하지 않는 듯 합니다만… 나름 AI 코딩 스타트업들이 많이 시도해볼 법한 방법이라고 봅니다.
결론?
이것저것 만들어서 잔뜩 고치긴 했는데, 이게 과연 리뷰 가능한 코드인지에 대해선 아직 의구심이 있습니다… 좀 더 다듬어봐야 할 것 같아요.