반응형
작성일: 2025년 2월 18일

 

 

소스 코드 분석 도구를 분류해본다면, "유료" or "무료"가 아닐까?
(나의 개인적 의견임 ^^)
내가 "유료", "무료"를 분류한 이유는 "유료" 분석 도구가 어마어마하게 비싸서이다. ㅠㅠ
작은 회사가 구입해서 쓸 수 있는 소프트웨어가 아니다.
너무 비싸서, 대기업이나 되야 유료 소스 코드 분석 도구의 유용함을 체감할 수 있다.

 

 

Blackduck Coverity (유료)

Blackduck Coverity는 많이 비싸다.

"Coverity 제품을 한번 써볼까?" 이런 느낌으로 쓸 수 있는 수준의 가격이 아니다. ㅠㅠ

어쨌든, 나는 Blackduck Coverity 분석 도구에 대한 도입을 고려하고 Blackduck Coverity 공급사에 문의 중이니까 의미있는 문의 결과가 있으면 여기에 추가로 내용을 공유할 생각임.

  • 지금까지 알아본 Blackduck Coverity의 가격 정책은 이렇다.
    • 최소 사용자: 10인
    • 1년 단위로 사용료 지불
    • 부가세까지 포함하여 10-User & 1년 사용인 경우, '현대차 아이오닉5 정도의 가격'.
      (Blackduck 영업 비밀이라서 정확한 가격을 말할 수 없음)
      • 10명이 Software를 개발하는데, 매년 이 정도의 돈을 내야 한다는 뜻이다. ㅠㅠ
    • 3년 이상 다년 동안 계약하면, 할인이 있다.

'Blackduck Coverity' 공급사의 담당자와 미팅을 하면, 5일간 Demo를 할 수 있는 Trial License(Demo License)를 준다.

5일간 Source Code 분석해보고, 정말 쓸모가 있다면 도입을 결정하는 것을 추천한다.

 

 

CppCheck (무료)

CppCheck에 대한 자세한 설명은 아래 웹 문서를 참고:

https://cppcheck.sourceforge.io/

 

CppCheck 프로그램 설치하기

## Debian, Ubuntu
$ apt install cppcheck
## macOS
$ brew install cppcheck
## Fedora
$ yum install cppcheck

 

CppCheck 명령 실행하기

내가 제일 많이 사용하는 옵션은 아래와 같다.

현재 디렉토리에 있는 모든 파일을 분석할거라면, 아래의 옵션으로 실행하는 것을 추천한다. 

$ cppcheck --check-level=exhaustive \
--enable=warning,style,performance,portability,unusedFunction ./

 

 

 

SonarQube

## 작성 보류.

 

 

 

 

기타 참고하면 좋은 문서

C++ 정적 코드 분석하기 - clang-tidy, cppcheck, 컴파일 옵션

https://nx006.tistory.com/37

 

 

 

 

반응형
작성일: 2025년 1월 21일

 

C언어로 작성된 Signal Handler 내에서  "localtime()" 함수를 사용할 때, 주의할 점이 있다.

아래 예제 코드처럼 program 내에서 이미 localtime() 함수가 사용되고 있다면, Signal Handler 내부의 localtime() 함수로 인해서 호출되는  futex_wait() 함수 부분에서 Deadlock(데드락) 상태로 빠질 수 있다.

 

아래 예제 소스 코드를 컴파일하고, 테스트해보면 1~5초가 지날 때쯤 deadlock 상태가 된다.

일반적으로 multi thread 환경에서 mutex lock 때문에 deadlock이 되는 경우는 많지만, 아래 예제 코드처럼 multi thread가 아닌데 deadlock 상태가 되는 경우는 경험하기 쉽지 않다.

 

만약, 아래 예제 코드에서 signal handler를 제거한다면 deadlock 상황은 발생하지 않는다.

 

/****************************************************
* How to compile
* $ gcc -g main.c -o myapp
* How to test
* $ ./myapp
***************************************************/
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
static void sigchd_handler(int signo)
{
time_t tv;
tv = time(NULL);
struct tm mytime = *localtime(&tv); // NOTE: 이 부분에서 Deadlock 발생함.
char my_date[64];
sprintf(my_date, "%d-%02d-%02d %02d:%02d:%02d",
mytime.tm_year + 1900, mytime.tm_mon + 1, mytime.tm_mday,
mytime.tm_hour, mytime.tm_min, mytime.tm_sec);
printf("%s(%d) %s() time: %s\n", __FILE__, __LINE__, __func__, my_date);
fflush(NULL);
return ;
}
int register_signal_handler(void)
{
struct sigaction sa;
sa.sa_handler = sigchd_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
return __LINE__;
}
#define LOOP_TRUE 1
int main(void)
{
int ret;
int idx;
ret = register_signal_handler();
time_t tv;
while (LOOP_TRUE)
{
printf("My PID: %d\n", (int) getpid());
FILE *fp = popen("ls -al", "r");
tv = time(NULL);
struct tm mytime;
for (idx = 0; idx < 10000; idx++)
{
printf("localtime() Function Call Counter: %d \r", idx);
fflush(NULL);
mytime = *localtime(&tv); // NOTE: 이 부분에서 Deadlock 발생함.
}
printf("\n");
char my_date[64];
sprintf(my_date, "%d-%02d-%02d %02d:%02d:%02d",
mytime.tm_year + 1900, mytime.tm_mon + 1, mytime.tm_mday,
mytime.tm_hour, mytime.tm_min, mytime.tm_sec);
printf("%s(%d) %s() time: %s\n", __FILE__, __LINE__, __func__, my_date);
fflush(NULL);
pclose(fp);
sleep(1);
}
return 0;
}

 

 

위 예제 코드에서 signal handler 때문에 deadlock이 발생하는 이유는 무엇일까?

localtime() 함수의 구현 부분을 보면, futex_wait() 함수를 호출하는 부분이 있다. 이 futex_wait() 함수가 이미 호출된 상태인데 또 한번 futex_wait() 함수가 호출되면 deadlock 상태가 된다. 즉, localtime() 함수가 호출 중인 상태에서 또 한번 localtime() 함수가 호출되면 동일 lock 변수인 tzset_lock 변수를 통해서 lock 시도를 했는데, 또 tzset_lock 변수를 이용해서 lock 시도는 하는 셈이 되는 것이다.

결국 tzset_lock -> locking 그리고 tzset_lock -> locking  이런 식으로 연속으로 두번 locking 시도를 하면서 2번째 locking 시도는 영원히 locking하려고 대기하게 된다. 또한 첫번째 locking 시도는 두 번째의 locking 시도를 한 signal_handler가 종료가 되지 않으니까 영원히 signal_handler의 종료만 기다리며 tzset_lock이 unlocking 되지 않는 것이다.

 

 

Deadlock 상태에 빠지는 이유를 이해했다면, 실습해보자!

아래와 같이 Source code를 compile하고, 바이너리 코드를 실행해보자.

실행하고 대략 1~5초 후에 프로세스가 아무것도 출력하지 않고 멈춘 것처럼 보일 것이다.

이때가 동일한 tzset_lock 변수에 대해서 첫번째 locking -> 두번째 locking을 연달아 시도하면서 deadlock 상태에 빠진 것이다.

$ gcc -g main.c -o myapp
$ ./myapp
My PID: 255676
main.c(25) sigchd_handler() time: 2025-01-21 07:54:58
localtime() Function Call Counter: 9999
main.c(72) main() time: 2025-01-21 07:54:58
My PID: 255676
localtime() Function Call Counter: 382 <-- 여기서 Deadlock 상태에 빠짐

 

 

머리로 이해한 것을 Process 내부의 function call stack을 통해서 확인해보자!

Deadlock 상태에 빠졌을 때, 아래와 같이 gdb로 function call stack을 확인해보면

  1. main()함수는 localtime()를 호출하면서 tzset_lock 변수를 통해 locking 시도를 하고 있었고,
  2. 이런 와중에 popen("ls -al", "r") 함수를 통해 실행되었던 child process가 종료되면서,
  3. Parent Process인 "myapp"이 SIGCHLD를 수신하게 된다.
  4. 이때 signal handler인 "sigchd_handler"가 실행되면서 localtime() 함수를 한번 더 실행하게 된다.
  5. 짠~!  main() 함수가 아직 localtime()함수를 통해 tzset_lock을 unlock하지 않았는데, signal handler의 localtime() 함수가 tzset_lock을 locking 시도를 했기 때문에 이때부터 dealock 상태가 되는 것이다.

 

$ gdb ./myapp 255676
(gdb) where
#0 futex_wait (private=0, expected=2, futex_word=0x7fa99d11c760 <tzset_lock>) at ../sysdeps/nptl/futex-internal.h:146
#1 __GI___lll_lock_wait_private (futex=futex@entry=0x7fa99d11c760 <tzset_lock>) at ./nptl/lowlevellock.c:34
#2 0x00007fa99cfd6744 in __tz_convert (timer=1737446099, use_localtime=1, tp=0x7fa99d11c6a0 <_tmbuf>) at ./time/tzset.c:572
#3 0x0000564ee1a0a30e in sigchd_handler (signo=17) at main.c:20
#4 <signal handler called>
#5 __GI___fstatat64 (fd=-100, file=0x7fa99d0d4b02 "/etc/localtime", buf=0x7ffdd9ca4780, flag=0)
at ../sysdeps/unix/sysv/linux/fstatat64.c:166
#6 0x00007fa99cfd683c in __tzfile_read (file=file@entry=0x7fa99d0d4b02 "/etc/localtime", extra=extra@entry=0,
extrap=extrap@entry=0x0) at ./time/tzfile.c:155
#7 0x00007fa99cfd5c24 in tzset_internal (always=<optimized out>) at ./time/tzset.c:405
#8 0x00007fa99cfd65a7 in __tz_convert (timer=1737446099, use_localtime=1, tp=0x7fa99d11c6a0 <_tmbuf>) at ./time/tzset.c:577
#9 0x0000564ee1a0a528 in main () at main.c:65
(gdb)

 

 

그러면, signal handler 내부의 localtime() 함수 때문에 deadlock이 생기는 상황을 피하려면 어떻게 해야 할까?

signal handler에서는 Lock 관련 함수/변수 사용 금지, I/O 관련 함수 호출을 하지 말라는 권고가 옛날부터 전해져 내려오고 있다.

signal handler는 최소의 작업만, 그리고 빠른 시간에 처리하고 종료되도록 구현하라는 권고도 있어왔다.

그러니까 localtime() 함수로 예쁘게 date 값을 뽑아내려고 하지 말고, 그냥 time(NULL)을 이용해서 epoch time 값을 뽑아서 활용하면 문제를 해결할 수 있다. 

signal handler 내부에서 Lock 관련 함수를 쓰지 않으면 만사 OK !!

반응형
작성일: 2025년 1월 8일

 

 

로그 함수를 사용하다보면, __FILE__, __LINE__ 같은 내용을 반복적으로 입력하게 된다.

타이핑치는 입장에서는 귀찮고, 코드를 읽는 입장에서는 사족 같은 느낌이 든다.

그렇다고 __FILE__, __LINE__ 같은 내용이 필요없는 내용은 아니다. 이런 정보가 디버깅할 때는 아주 중요한 단서를 제공한다.

 

가변 인자 매크로(__VA_ARGS__)를 이용하면, __FILE__, __LINE__ 같은 반복되는 내용을 줄일 수 있다.

아래 예시를 보면, 딱 느낌이 올 것이다.

 

#include <stdio.h>
#include <stdarg.h>
// __VA_ARGS__ 매크로 상수를 사용
#define MYLOG(...) mylog(__FILE__, __LINE__, __VA_ARGS__)
void mylog(char * file, int line, char * format, ...)
{
fprintf(stdout, "(%s:%d) ", file, line);
va_list args;
va_start(args, format);
vfprintf(stdout, format, args);
va_end(args);
return ;
}
int main()
{
// 테스트 변수
int num_a = 1;
char str_a[16] = "AAA";
// log 함수 사용 예시
mylog(__FILE__, __LINE__, "This is first log. num_a:%d str_a:%s\n", num_a, str_a);
// Log Macro 사용 예시
// __FILE__, __LINE__ 을 사용하지 않았지만, MYLOG 매크로를 이용하여 file name과 line 정보를 출력함.
MYLOG("This is second log. num_a:%d str_a:%s\n", num_a, str_a);
return 0;
}

 

 

위 소스코드를 컴파일하고, 실행해보면 아래와 같이 동작한다.

$ gcc main.c
$ ./a.out
(main.c:26) This is first log. num_a:1 str_a:AAA
(main.c:29) This is second log. num_a:1 str_a:AAA
$

 

 

반응형
작성일: 2024년 12월 17일

 

Binary 파일이 정확히 언제 Build된 것인지 궁금할 때가 있다.

이럴 때 아래와 같이 Makefile과 C source code를 작성하여,

Binary file에 Build Date 정보를 삽입하고

`strings` 명령어로 build date를 조회할 수 있다.

 

 

Makefile 작성하기

파일 이름: Makefile

MY_APP_BUILD_DATE := $(shell date "+%Y-%m-%d %H:%M:%S")
CC=gcc
CFLAGS=-D__MY_APP_BUILD_DATE__='"$(MY_APP_BUILD_DATE)"'
all:
$(CC) -o myapp main.c $(CFLAGS)

 

C source code 작성하기

파일 이름: main.c

#include <stdio.h>
int main(void)
{
printf("App Build Date: %s\n", __MY_APP_BUILD_DATE__);
return 0;
}

 

 

 

Build Date 확인하기

 

$ make
gcc -o myapp main.c -D__MY_APP_BUILD_DATE__='"2024-12-17 13:14:28"'
$ ./myapp
App Build Date: 2024-12-17 13:14:28
$ strings myapp
App Build Date: %s
2024-12-17 13:14:28

 

 

 

 

 

 

 


 

반응형

 

작성일: 2024년 12월 10일

 

 

Segmentation fault (core dumped)

 

분명 process가 segmentation fault 상황에서 core file이 dump되어야 하는데, 어디에도 core file이 없는 경우가 있다.

이럴 때는 coredump와 관련한 설정이 안 되어 있는 것이니까, 아래의 내용을 보고 coredump 관련 설정이 잘 되어 있는지 다시 확인해봐야 한다.

 

core dump file의 size 제한 설정이 있는지 확인

너무 큰 core dump file이 생성되는 부작용을 막기 위해 core file size limit 설정이 존재한다.

(2020년대 이후로는 SSD, HDD가 충분히 커서 이런 core file size limit이 유용한지 모르겠다.)

core file이 수십개가 dump되어도 문제 없을만큼 storage가 크다면, unlimit 설정값으로 바꾸는 것을 권장한다.

## 현재 설정값 확인하는 명령.
$ ulimit -a
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
... (내용 생략) ...
## core file size 제한을 푸는 명령 (무제한 크기로 변경)
$ ulimit -c unlimited

 

위와 같이 설정하면, 내 login shell 환경에만 즉시 적용되고  Linux 시스템 전체적으로는 아직도 옛날 core file size가 적용된다.

따라서 Linux 시스템 전체에 설정 값을 적용하려면 아래와 같이 limits.conf 파일을 수정해야 한다.

$ cat /etc/security/limits.conf
(... 중간 내용 생략 ...)
* soft core unlimited
(... 중간 내용 생략 ...)

 

 

core file 이름 패턴 및 경로 설정

core file이 어느 폴더에 저장되고, 어떤 이름으로 저장될지 설정해야 한다.

$ cat /proc/sys/kernel/core_pattern
/tmp/core.%e.%p.%h.%t

 

각 설정값이 의미하는 것은 이렇다.

  • %e   프로그램 이름 (실행 binary file name)
  • %p   Process ID (예: 3453)
  • %h   Host name (예: mynode)
  • %t    core file dump된 시각 

 

위와 같이 설정하면, 아래와 같이 실제로 core file이 /tmp 폴더에 dump된다.

$ ls /tmp/core.*
/tmp/core.myapp.104618.mynode.1733790395

 

위 파일 이름에 포함된 "1733790395" 값은 Unix time(또는 epoch time)라고 한다.

이 epoch time을 우리 눈에 익숙한 포맷으로 바꾸려면 아래와 같이 명령을 수행하면 된다.

## Timezone 설정이 UTC이면, 아래와 같이 출력
$ date -d @1733790395
Tue 10 Dec 00:26:35 UTC 2024
## Timezone 설정이 Asia/Seoul이면, 아래와 같이 출력
$ date -d @1733790395
Tue Dec 10 09:26:35 KST 2024

 

 

epoch time 및 date 포맷 변경에 관한 자세한 내용은 아래 블로그 글을 참고

https://andrewpage.tistory.com/193


 

반응형
작성일: 2024년 12월 10일

 

 

대화형 방식으로 gdb 명령 사용하기 (interactive mode)

## 현재 실행중인 프로세스 '1234'에 attach하기 위해 아래와 같이 명령을 실행
$ gdb -p 1234
## 프로세스 '1234'의 모든 thread 목록을 출력
$ info thread
... ( 출력 내용 생략 ) ...
... 모든 쓰레드 정보가 출력됨 ...
## 전체 thread 중에서 thread '5'의 정보를 보기
$ thread 5
## thread '5'의 backtrace 정보를 출력
$ bt

 

 

 

batch 방식으로 gdb 명령 사용하기 (non-interactive mode)

gdb 명령을 batch 방식으로 사용할 수 있다. 

아래 예제처럼 2개의 gdb 내부 명령을 사용하고자 할 때는 '-ex' 옵션을 2번 사용하면 된다.

이렇게 하면, -ex 옵션 순서에 따라(즉, 왼쪽에서 오른쪽으로 순서에 따라) 명령이 차례대로 수행된다. 

$ gdb -batch -ex "info thread" -ex "thread apply all bt" -p 1234

 

위 예제의 경우,

  - 전체 thread list를 출력하고,

  - 각 thread의 call stack을 출력한다.

 

 

 

 

 


 

반응형

C언어로 Multi thread 구조를 만들다보면 자연스럽게 사용하게 되는 mutex lock과 conditional variable에 관한 예제이다.

 

 

#include "sys/time.h"
#include "sys/types.h"
#include <sys/poll.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define ITER 30
int g_data[ITER];
pthread_cond_t cond[ITER];
pthread_mutex_t mutex[ITER];
void* producer(void *data){
int rc;
int idx = 0;
while (1) {
int idx_mod = idx % ITER;
rc = pthread_mutex_lock(&mutex[idx_mod]);
g_data[idx_mod] = idx;
#ifdef __STDOUT_PRINT__
printf("producer.data: %d\n", g_data[idx_mod]);
fflush(NULL);
#endif
rc = pthread_cond_signal(&cond[idx_mod]);
rc = pthread_mutex_unlock(&mutex[idx_mod]);
poll(0, 0, 1);
idx++;
}
}
void* consumer(void *data){
int rc;
int my_thr_idx = *(int *)data;
printf("consumer thread idx: %d\n", my_thr_idx);
while (1) {
rc = pthread_mutex_lock(&mutex[my_thr_idx]);
rc = pthread_cond_wait(&cond[my_thr_idx], &mutex[my_thr_idx]);
#ifdef __STDOUT_PRINT__
printf("comsumer[%d].data: %d\n\n", my_thr_idx, g_data[my_thr_idx]);
fflush(NULL);
#endif
rc = pthread_mutex_unlock(&mutex[my_thr_idx]);
}
}
int main()
{
//
// producer thread
//
pthread_t thr_id_prod;
if (pthread_create(&thr_id_prod, NULL, producer, NULL) != 0) {
printf("thread create error(%d, %s)\n", errno, strerror(errno));
return 0;
}
poll(0, 0, 1000);
//
// consumer thread
//
pthread_t thr_id_cons[ITER];
int new_idx[ITER];
for (int idx=0; idx<ITER; idx++) {
new_idx[idx] = idx;
pthread_mutex_init(&mutex[idx], NULL);
pthread_cond_init(&cond[idx], NULL);
if (pthread_create(&thr_id_cons[idx], NULL, consumer, (void *) &new_idx[idx]) != 0) {
printf("thread create error(%d, %s)\n", errno, strerror(errno));
return 0;
}
}
int status;
for (int idx=0; idx<ITER; idx++) {
pthread_join(thr_id_cons[idx], (void **)&status);
}
return 0;
}

 

위와 같이 코드를 작성하고, gcc compiler로 실행 파일을 만든다.

 

$ gcc -D__STDOUT_PRINT__ main.c -o my_test_app
$ ./my_test_app

 

 

 

+ Recent posts