본문 바로가기

공부기록/42 Seoul

[ft_printf] 01 printf 함수에 대하여

printf 출력 형식의 구조

%[flag][width][.정밀도(precision)][길이(length)]서식지정자(specifier)

구현해야할 것

[Mandatory]

  • 다음의 conversion을 구현. %c %s %p %d %i %u %x %X %%
  • 다음 flag들의 동작을 구현. - 0 . *

[Bonus]

  • %n %f %e %g 중 하나 이상의 conversion 구현
  • l ll h hh 중 하나 이상의 flag 구현
  • + (공백) # flag 구현.

 

공부해야할 것

(몰랐던 것만 정리했다. 티스토리 표 디자인 진심 구리네)

 

서식문자(specifier)

%i 부호가 있는 10진 정수. %d 는 다음과 같은 차이가 있다. →https://hashcode.co.kr/questions/934/형식지정자-i-d-차이
%u 부호가 없는 10진 정수
%x 부호가 없는 16진법 정수 (소문자 사용)
%X 부호가 없는 16진법 정수 (대문자 사용)
%% stdout에 %를 문자 그대로 출력
%n 아무것도 출력하지 않는다. 그 대신, 인자로 부호 있는 int형을 가리키는 포인터를 전달해야 되는데, 여기에 현재까지 쓰여진 문자 수가 저장된다.
%g %f와 %e중 더 짧은 것을 사용

플래그(flag)

- 왼쪽 정렬
0 수들을 오른쪽으로 정렬하되 빈 칸을 삽입하는 대신에 0 삽입.
(문자열에 대해선 undefined behavior. -와 같이 쓰일경우 - 가 우선순위 높음)
* 폭(width) : 너비를 인자로 받는다.
width 값이 음수면 - flag 가 있는 것 처럼 왼쪽 정렬된다.
  예시
printf("iNum : %*d\n", 5, iNum);
printf("fNum : %*f\n", 30, fNum);
printf("결과 : %+*s\\n", 14, "문자열 자르기"); 

//결과
iNum :    65
fNum :                    -26.230000
결과 :        문자열 자르기
(공백) 앞에 부호가 붙지 않는다면 한 칸을 출력한다. (다시말해 123은 " 123" 으로 출력되고 -123은 "-123"으로 출련된다.)
# o, x, X 서식 문자들과 사용되면 출력되는 값 앞에 각각 0, 0x, 0X 가 붙게 된다. (출력값이 0일 땐 제외).
e, E, f 서식 문자들과 사용되면 소수점 아래 수들이 없음에도 불구하고 강제적으로 소수점을 붙이도록 한다. 원래 소수점 아래 수들이 없다면 소수점을 붙이지 않는다.
g 와 G 서식 문자들과 사용되면 e 와 E 일때와 동일한 작업을 하지만 소수들의 뒷부분에 붙는 0 들 (123.1200 등) 은 제거되지 않는다.
+ 출력 결과값이 양수인 경우라도 + 기호를 앞에 붙여서 출력하도록 한다. (물론 음수면 자동적으로 - 가 붙는다). 기본적으로 지정하지 않았을 경우 음수에만 앞에 - 가 붙는다.

길이(length)

ll long long int, unsigned long long int
h short int, unsigned short int
hh signed char, unsigned char

 

 

 


 

 + ) restrict 포인터

printf 메뉴얼을 보니 함수 원형에 restrict * 가 붙어있어서 호기심에 공부했다.

//printf 함수원형
int printf(const char * restrict format, ...);

이런 함수가 있다고 하자

//increase 함수
void increase(int *a, int *b, int *x)
{
    *a += *x;
    *b += *x;
}

 

어셈블리 뜯어보는 방법

더보기
$ gcc -g -std=c99 -O3 -c increase.c
$ objdump -S increase.o

 

항목

설명

-g

gdb에게 제공하는 정보를 바이너리에 삽입한다.
(-g 옵션을 사용하지 않고 gdb로 디버깅하면, 역어셈 **** 어셈블리 코드로만 디버깅 가능)

-std=c99

(C언어와 관련된 옵션) c99 표준 지정

-O3

가장 높은 레벨의 최적화. 모든 함수를 인라인 함수와 같이 취급한다.

-c(파일명.c)

오브젝트 파일을 생성한다.

   

objdump

목적파일의 내용을 덤프하는 것. 덤프는 짐을 내린다는 뜻인데, 기억 장치의 내용을 일부 또는 전부를 출력한 복사본을 의미

-s

section 별로 나눠서 내용를 보여줌

결과

0000000000000000 _increase:
; {
       0: 55                pushq	%rbp
       1: 48 89 e5          movq	%rsp, %rbp
;     *a += *x;
       4: 8b 02             movl	(%rdx), %eax  //x를 역참조하여 가져온 값을 eax에 저장
       6: 01 07             addl	%eax, (%rdi)  //eax의 값만큼 a를 역참조하여 값을 증가시킴. rdi에 eax만큼 더한다.
;     *b += *x;
       8: 8b 02             movl	(%rdx), %eax  //x를 역참조하여 가져온 값을 eax에 저장
       a: 01 06             addl	%eax, (%rsi)  //eax의 값만큼 b를 역참조하여 값을 증가시킴
; }
       c: 5d                popq	%rbp
       d: c3                retq

함수 호출 규약에 의하면, 매개변수는 레지스터를 통해 전달되고, 다음의 순서로 레지스터를 이용한다.
rdi 첫번째 매개변수
rsi 두번째 매개변수
rdx 세번째 매개변수

 

본론으로 돌아와서, restrict 가 필요한 이유는 뭘까?  

int a = 1;
increase(&a, &a, &a);

이렇게 호출하면 매개변수 a, b, x는 같은 메모리 공간을 가리키게 된다. 이런 포인터를 alias라고 부르는데, 이런 경우 잘못된 처리를 되돌리는 작업이 복잡하다.  

그래서 alias가 아닌 상황에서, 어셈블리를 더 최적화할 수 있기 위해 restrict 포인터라는 것이 존재한다.

void increase(int *restrict a, int *restrict b, int *restrict x)
{
    *a += *x;
    *b += *x;
}

결과

0000000000000000 _increase:
; {
       0: 55                            pushq   %rbp
       1: 48 89 e5                      movq    %rsp, %rbp
;     *a += *x;
       4: 8b 02                         movl    (%rdx), %eax
       6: 01 07                         addl    %eax, (%rdi)
;     *b += *x;
       8: 01 06                         addl    %eax, (%rsi)
; }
       a: 5d                            popq    %rbp
       b: c3                            retq

*b += *x 의 어셈블리 명령이 1줄 줄어들었다. 매개변수 x의 위치가 a, b와 다르다는 것이 확실하기 때문에 x의 위치를 다시 역참조하하는 movl (%rdx), %eax가 필요없어진 것이다.  

 

그러니까 restrict 포인터는 컴파일러에게 최적화를 하라고 알려주는 키워드이다. restrict가 있다고 무조건 메모리가 다른 공간을 가리킨다고 보장하는게 아니므로, 사용할 때 주의하여야한다.

 

 

 

참고

https://www.cypress.com/file/54441/download

https://dojang.io/mod/page/view.php?id=760

 

C 언어 코딩 도장: 85.16 restrict 포인터

restrict 포인터는 메모리 접근에 관련된 최적화 기능입니다(C99 표준). 예를 들어 다음과 같이 포인터를 역참조하여 값을 증가시키는 코드가 있습니다. increase.c void increase(int *a, int *b, int *x) { *a += *

dojang.io