1. 초기값을 갖는 전역변수
char msg[] = “Hello”;
의미는 int의 초기값과 같은 구조로 처리 된다. 이것은 msg 변수 초기값을 갖는 변수 영역에 할당하고 프로그램 실행 전에 “hello”라는 string이 전송되고 프로그램 실행 되는 것이다. 그러나 다음과 같은 코드를 생각해 보자.
char msg[100];
msg = “Hello”;
와 같은 코딩은 불가능하다. 이것은 msg라는 변수영역에 string을 복사하는 것이다. 위에서 언급 했듯이 ‘=’이라는 기본적으로 CPU가 한번의 기계어 동작에 의해 전송되는 것을 의미 하기 때문이다.
#include "stdafx.h"
char msgh[100] = "Hello";
char msg[100];
void pmsg()
{
msg = "Message";
char *pmsg = "Message";
printf(pmsg);
}
------- Configuration: ClOpt - Win32 Release ---------------------–
Compiling...
fchar.cpp
D:...fchar.cpp(9) : error C2440: '=' : cannot convert from 'char [8]' to 'char [100]'
There is no context in which this conversion is possible
Error executing cl.exe.
ClOpt.exe ‐ 1 error(s), 0 warning(s)
이것은 Visual C++로 컴파일 한 것인데, 만약 빨간색의 ‘msg = "Message";’을 제거 하면 error가 사라지고 문제가 없음을 확인할 수 있다.
이에 비해 이것을 포인터로 바꾸면
char *pmsg;
pmsg = “Hello”;
는 가능한데 여기서의 ‘=’은 string “hello”가 있는 주소 값을 pmsg 변수 넣는 것이므로 위의 내용에 위배되지 않는다.
char msg[100];
char *pmsg;
char *pdmsg;
void prtmsg()
{
pmsg = "Message";
pdmsg = msg;
while (*pmsg) *pdmsg++ = *pmsg++;
*pdmsg = (char) 0;
printf(msg);
}
_BSS SEGMENT
_msg DB 064H DUP (?) ; char msg[100];
_pmsg DD 01H DUP (?) ; char *pmsg;
_pdmsg DD 01H DUP (?) ; char *pdmsg;
_BSS ENDS
PUBLIC _Message ; `string'
EXTRN _printf:NEAR
; COMDAT _Message
_DATA SEGMENT
_Message DB 'Message', 00H ; `string'
_DATA ENDS
; COMDAT _prtmsg
_TEXT SEGMENT
_prtmsg PROC NEAR ; prtmsg, COMDAT
mov al, BYTE PTR _Message
mov ecx, OFFSET FLAT:_msg ; msg
test al, al
mov DWORD PTR _pmsg, OFFSET FLAT:_Message ; pmsg, `string'
mov DWORD PTR _pdmsg, ecx ; pdmsg
. . .
위의 어셈블리에서 string “Hello”는 정해진 위치(_DATA SEGMENT)에 string의 데이터 형태로 존재하고 이것의 포인터가 ‘pmsg = "Message";’에 의해 포인터 값이 변수의 address에 저장 된다. 여기서 다시 한번 강조하면 ‘=’이 string 전체를 복사하는 것이 아니라는 것이다.
2. 주소 변환 시 포인터 변수와 []변수와는 같은 것이다
char data[100];
char *pch;
pch = data;
pch[0] = ‘H’; pch[1] = ‘e’;
와 같은 표현이 가능하다.
이것은 컴파일러가 기계어 코드를 생성할 때, 같은 원리로 동작함을 알 수 있다.
pch[1] => pch+1과 같고 => 주소값은 pch의 주소값 + sizeof(char)*1
=> 0x30001200+1*1 = 0x30001201
[]을 사용 하는 함수의 예를 들면
void strcpy(char *dest, char *src)
{
int cnt;
for (cnt = 0;src[cnt];cnt++) {
dest[cnt] = src[cnt];
}
dest[cnt] = (char) 0;
}
이 함수 같은 기능의 포인터를 이용한 프로그램으로 한다면
while (*src) {
*dest++ = *src++;
}
두 프로그램의 기능은 같지만 컴파일 코드는 많은 차이가 있다.
‘dest[cnt] = src[cnt];’에서 ‘dest[cnt]’는
dest의 시작주소 + sizeof(char)*cnt
와 같은 복잡한 계산을 한다면
*dest++는 ‘*dest’와 dest++의 결합으로
주소 계산없이 바로 문자를 넣고 포인터의 주소값을 sizeof(char) 만큼 만 증가하면 된다.
위의 2개의 차이는 프로그램 상에서 어느 것을 선택 코딩 하느냐에 따라 실행 능력에 많은 영향을 미친다는 것에 주목해야 한다.
char의 정수형 의미
char는 문자를 취급하는 변수인데, 이것의 의미는 무엇인가. 이것과 관련해 문자 코드란 무엇인가와 연결되어 있다. 이 변수 타입과 관련해 초기의 도입목적은 문자를 취급 한다는데 있지만 실제 사용과 응용의 측면을 볼 때 반드시 문자만을 취급하는 개념을 넘어 선다. 오히려 char는 byte 단위의 처리를 의미 한다. 즉, CPU가 메모리나 레지스터를 억세스 할 때, byte 단위로 이루어 졌다면 모두 char을 사용하는 것이다. 이 말은 다시 말해 숫자도 한 바이트 내에 있다면 char 인 것이다. 따라서 char는 바이트로 처리하는 단위를 말하며, 이것은 정수형과 구별되지 않고 같은 구조로 이루어 지는 것이다.
정수형은 처리 단위가 CPU 레지스터와 연관지어 사용되고 ALU을 통한 연산이 이루어 지는 것이다. char 역시 어떤 바이트 값이 처리되어 CPU 레지스터로 오면 정수형의 모든 연산이 가능 하다. 예를 들어 ASCII의 소문자를 대문자로 바꾸는 경우, 일단은 메모리로부터 읽어 레지스터로 저장한 다음 정수 연산을 통해 다시 새로운 값으로 변형이 가능한 것이다.
소문자를 대문자로
char ch;
if (ch >= ‘a’ && ch <= ‘z’)
ch = ch – ‘a’ +’A’;
여기서의 연산은 ch 변수의 값이 레지스터로 전송되고 다음은 산술연산 중 +,‐을 이용해 대문자로 바꾼 것이다.
여기서 바꾸는데 사용한 코드 ‘ch = ch – ‘a’ +’A’;’는 다음과 같이 기계어로 개념화 할 수 있다.
sub.b r0, 32 ; ‘A’‐ ‘a’=> ‐32
intel 80x86은
sub al, 32
이 기계어 코드에서 ch 변수를 int로 바꾸면 다음과 같이 기계어가 처리 단위가 바뀌고 나머지는 CPU의 연산 체계가 같음을 알수 있다.
int ch;
if (ch >= ‘a’ && ch <= ‘z’)
ch = ch – ‘a’ +’A’;
sub.l r0, 32
80x86 : sub eax, 32 ; 00000020H
여기서 char가 CPU에서의 처리 단위을 규정함을 알 수 있다.
다음 프로그램에서 다시 한번 고려 해 본다.
char cval1;
char cval2;
unsigned char ucval1;
unsigned char ucval2;
int main(int argc, char* argv[])
{
cval1 = 'a';
cval2 = cval1 + 0x60;
ucval1 = 'a';
ucval2 = ucval1 + 0x60;
//. . .
}
이 프로그램을 기계어로 컴파일하면 다음과 같다. 컴파일러는 VC++ 6.0이다. 컴파일마다 기계어 코드가 달리 나오나 원리적인 문제를 학습하는데 문제가 없다.
전역변수는 _BSS segment에 할당하고 다음과 같이 코딩 된다.
PUBLIC _cval1 ; cval1
PUBLIC _cval2 ; cval2
PUBLIC _ucval1 ; ucval1
PUBLIC _ucval2 ; ucval2
_BSS SEGMENT
_cval1 DB 01H DUP (?) ; cval1
ALIGN 4
_cval2 DB 01H DUP (?) ; cval2
ALIGN 4
_ucval1 DB 01H DUP (?) ; ucval1
ALIGN 4
_ucval2 DB 01H DUP (?) ; ucval2
_BSS ENDS
; cval1 = 'a'; 'a' == 97 (ASCII 코드)
mov BYTE PTR _cval1, 97 ; cval1, 00000061H
; cval2 = cval1 + 0x60;
movsx eax, BYTE PTR _cval1 ; cval1
add eax, 96 ; 00000060H
mov BYTE PTR _cval2, al ; cval2
; ucval1 = 'a';
mov BYTE PTR _ucval1, 97 ; ucval1, 00000061H
; ucval2 = ucval1 + 0x60;
xor eax, eax
mov al, BYTE PTR _ucval1 ; ucval1
add eax, 96 ; 00000060H
mov BYTE PTR _ucval2, al ; ucval2
코딩 부분을 보면 부호가 있는 char을 계산하는 것이다.
; cval1 = 'a'; 'a' == 97 (ASCII 코드)
mov BYTE PTR _cval1, 97 ; cval1, 00000061H
_cval1의 영역에 'a'을 넣고
다음의 char 연산을 계산하기 위해 다음과 같이 기계어로 바뀐다.
; cval2 = cval1 + 0x60;
movsx eax, BYTE PTR _cval1 ; cval1
byte을 32비트의 레지스터로 변환 옮기기 위해 movsx을 사용 하였다. 이것은 부호를 유지하기 위해 이 기계어로 컴파일 된 것이다.
add eax, 96 ; 00000060H
32비트로 계산이 되고 이것의 한 바이트만 cval2에 전송된다.
mov BYTE PTR _cval2, al ; cval2
이 기계어 코드가 말하는 것은 부호가 있는 char 변수는 레지스터로 계산할 때, int와 개념적 차이가 없음을 나타 낸다.
이와 비교하기 위해 unsigned char 의 기계어 부분을 살펴보면
; ucval1 = 'a';
mov BYTE PTR _ucval1, 97 ; ucval1, 00000061H
; ucval2 = ucval1 + 0x60;
xor eax, eax
우선 32비트를 사용하기 위해 EAX 레지스터를 지운다. 그리고는 다음 줄과 같이 8비트 만을 전송한다.
mov al, BYTE PTR _ucval1 ; ucval1
이러면 32비트의 앞부분이 0으로 채워 지면서 unsigned 가 유지 된다.
add eax, 96 ; 00000060H
32비트로 계산을 하고
mov BYTE PTR _ucval2, al ; ucval2
ucval2에 저장 한다.
위에서 VC++6.0은 char을 계산할 때 int로 변환하여 계산함을 볼수 있고 int형의 2의 보수 체계를 통한 연산을 유지함을 알 수 있다.
기계어로 바꿀 때, 컴파일 마다 차이가 있는데 이번에는 차이만을 비교하기 gcc로 컴파일 해 본다.
;char cval1;
;char cval2;
;unsigned char ucval1;
;unsigned char ucval2;
.globl _cval1
.bss
_cval1:
.space 1
.globl _cval2
_cval2:
.space 1
.globl _ucval1
_ucval1:
.space 1
.globl _ucval2
_ucval2:
.space 1
; cval1 = 'a'; 'a' == 97 (ASCII 코드)
movb $97, _cval1
; cval2 = cval1 + 0x60;
movzbl _cval1, %eax
addb $96, %al
movb %al, _cval2
; ucval1 = 'a';
movb $97, _ucval1
; ucval2 = ucval1 + 0x60;
movzbl _ucval1, %eax
addb $96, %al
movb %al, _ucval2
VisualC++의 컴파일 방법과는 다른데 여기서는 EAX 레지스터의 값을 변환 할 때, unsigned이든 그냥 char 이든 32비트로 확장하고 않고 8비트의 연산을 통해 부호를 처리함을 볼 수 있다.
char의 포인터와 포인터 계산 그리고 비교 이해
포인터는 CPU가 결정되면 주소 값을 취급하기 위한 바이트 수는 결정된다. MCU의 경우는 section에 따라서도 주소값을 취급하는 바이트는 다른 수 있다. 80x86등의 32비트는 주소값 역시 32비트 이다. 그런데 이 주소값을 계산할 때가 생기는데 CPU의 레지스터의 값을 ALU을 통해 계산되는 방식을 사용 한다.
다음은 string중에서 처음 이름을 다른 데이터와 분리하여 이름만을 추출하는 프로그램 예을 통해 이해해 본다.
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
char buff[1024] = "Kim 12 23 43";
char name[100];
int main(int argc, char* argv[])
{
char *dstr;
char *sstr;
char *nstr;
// buff에서 SPACE을 기준으로 다음 인수를 찾는다.
nstr = buff;
while (*nstr && *nstr != ' ')
nstr++;
// 이번에는 분리된 곳까지 짤아 이름을 분리한다.
sstr = buff;
dstr = name;
//while (sstr < nstr) {
while (1) {
if (sstr >= nstr) break;
*dstr++ = *sstr++;
}
*dstr = (char) 0;
printf("Name = %sn", name);
/* 다음은 정수형의 비교를 보기 위한 프로그램 */
srand( (unsigned)time( NULL ) );
int iv1, iv2;
iv1 = rand();
iv2 = rand();
if (iv1 >= iv2)
iv1 = iv2;
return iv1;
}
우선 char 포인터의 비교를 보면 다음과 같이 기계어로 컴파일 된다. (VC++6.0)
; if (sstr >= nstr) break;
cmp eax, esi
jae SHORT $L1005 ; 만약 조건 만족하면 break
여기서 보면 2 포인터를 비교하기 위해 cmp라는 기계어 코드가 보인다. 이것은 두 레지스터의 값을 빼고 FLAG을 setting 하고 뺀 값은 버린다.
이것과 int의 비교를 보면
; if (iv1 >= iv2)
cmp esi, eax
여기서도 포인터와 마찬가지의 구성으로 코딩 된다.
그런가 하면
sstr++과 같은 포인터 증가는
inc eax
와 같은 기계어로 코딩 된다.
이와 같이 char의 포인터 뿐만 아니라 모든 포인터는 CPU의 레지스터와 ALU과 결합되어 정수처럼 계산된다.
char의 8비트 데이터 처리와 구조
char 변수는 단순히 string 처리만을 하는 것은 아니라 8비트 처리의 총체라고 이미 언급 했다. 이것은 다른 많은 데이터의 처리에서 문자가 아닌 숫자가 사용되는데 이것이 8비트라면 char을 사용하면 되는 것이다. 물론 숫자를 int로 처리할 수 있지만 메모리라든가 처리 속도 등을 고려하여 char을 사용할 수 있다. 그리고 struct와 연계하여 처리되어 질 때도 byte라는 개념이 들어가면 무조건 char 변수를 사용하면 된다. 만약 통신용 프로그램을 작성하려 할 때, 버퍼를 잡을 때 byte 단위로 char array로 잡고 struct로 포맷을 만들어 포인터 개념을 사용하여 프로그램 할 수도 있다.
예를 들어 두 시스템간 통신 프로그램 작성을 할 때, 통신 포맷이 존재한다. 이 포맷은 통신이 진행되면서 여러가지 역활을 하는데 메모리 입장에서 byte로 잡고 조작할 수 있다.
다음 형태의 포맷을 구성해 보면
FORMAT ST : DTYPE : LENGTH : DATA : CRC32
OCTET 1 1 2 가변(32비트 어래인지) 4
각 요소를 간단히 정의 하면
l ST : Start Code 0x03으로 정의
#define ST_CODE 0x03
l DTYPE : 다음에 나오는 데이터의 취급 방법 기술인데 다음과 같은 종류를 정의 한다.
#define DTYPE_RAW 1 ‐ BYTE 형태의 데이터
#define DTYPE_STRING 2 ‐ C의 string의 형태
#define DTYPE_INT 3 ‐ 정수형 32비트의 데이터의 나열이다.
목적에 맞는 설정을 하면 된다.
l LENGTH: 다음 부분부터의 데이터 길이를 정의한다. 단위는 바이트이다.
l DATA : DTYPE의 형태에 따라 데이터가 온다.
l CRC32 : 데이터의 조합이 맞는지를 검토하기 위한 32bit CRC
이를 프로그램 하기 위해 다음과 같은 struct을 정의할 수 있다.
#define ST_CODE 0x03
#define DTYPE_RAW 1 // BYTE 형태의 데이터
#define DTYPE_STRING 2 // C의 string의 형태
#define DTYPE_ID 3 // 정수형 32비트의 데이터의 나열이다.
// 다음의 각 비트 단위는 CPU 마다 다르므로 주의 해야 한다.
typedef unsigned int u32;
typedef unsigned short int u16;
typedef unsigned char u8;
typedef struct CPacket {
char startcode;
char dtype;
u16 length;
char data[4]; // 가변 길이 전송할 데이터
u32 crc; // CRC32
} CPacket;
이 struct 구조는 프로그램의 용이성과 패킷의 형태를 결정하여 만든다. struct을 구성할 패킷은 처음의 char 변수 2개로 16비트를 맞추고 다음 length로 16비트를 맞춘다. 그러면 앞에서 3개 변수가 32비트가 된다. 다음은 데이터가 들어가는데 이것의 길이는 가변이므로 4바이트로 정의한 것은 특별한 의미가 없고 데이터의 위치만은 나타내고 프로그램 할 때, 포인터 조작을 위한 것이다. 다음의 CRC32의 위치는 위의 변수가 가변의 길이이기 때문에 의미는 없고 단지 CRC32가 데이터 다음에 나온다는 표시를 시각적으로 한 것이다. 이 말은 패킷을 만들어 보아야 길이가 결정되고 length 변수와 함께 crc의 위치를 계산할 수 있다.
위의 예와 같은 경우는 개발자 임의 정의한 것인데 실제로 통신의 경우는 표준화 때문에 개발자 마음대로가 되지 않으므로 상황 맞추어 struct을 잡는다.
이를 이용하여 데이트를 조합하여 패킷을 만들면
char txBuff[1024];
int makePacketA(CPacket *pkt, char type, void *data, int leng)
{
int cnt;
int crcCalcLeng; // CRC32을 계산할 때 길이를 나타낸다.
pkt-> startcode = ST_CODE; // (1) Start Code 넣기
pkt-> dtype = type; // (2) 패킷 타입 결정
switch (type) {
case DTYPE_RAW : // 1 - BYTE 형태의 데이터
{
unsigned char *dt;
unsigned char *srct;
dt = (unsigned char *) pkt->data; // (3) 데이터 넣기
srct = (unsigned char *) data;
for (cnt = 0; cnt < leng;cnt++)
*dt++ = *srct++;
while (cnt & 0x0003) { // (4) 더미 데이터 넣기 - 32비트 어래인지,
// struct의 데이터 타입을 원할히 하기위해
*dt++ = 0;
cnt++;
}
pkt->length = (u16) (cnt // (5) data 길이 계산하여 넣기 – 가변 길이
+ sizeof(u32) ); // CRC 바이트
// (6) CRC32 계산 하기
crcCalcLeng = sizeof(char) // char startcode;
+ sizeof(char) // char dtype;
+ sizeof(u16) // u16 length;
+ cnt; // data length
*(u32 *)dt = (u32)fn_calc_memory_crc32((void *) pkt, crcCalcLeng);
printf("DTYPE_RAW : Data Length for CRC Calc.= %dn", crcCalcLeng);
return (int) crcCalcLeng + sizeof(u32); // (7) 패킷 전체 길이 return
}
. . .
이를 이용하여 데이트를 조합하여 패킷을 만들면
(1) Start Code 넣기
(2) 패킷 타입 결정
(3) 데이터 넣기
(4) 더미 데이터 넣기 : 32비트 어래인지를 한것인데, 이것은 struct의 데이터 타입을 맞추지 않는면 수신 측에서 struct을 사용하여 프로그램을 할 때 다음의 32비트 CRC을 4바이트를 비트 연산으로 32비트로 만들어야 하는 수고가 따른다.
프로그램과 처리속도를 고려하여 더미 데이터가 0~3바이트까지 들어간다. 대신 데이터의 통신을 해야하는 통신상의 더미를 더 보내야 하는 희생을 하여야 한다.
(5) data 길이 계산하여 넣기 – 이것은 데이터의 길이가 가변으로 설정하여 프로그램하기 때문에 포멧상 길이 데이터를 추가하였다.
(6) CRC32 계산 하기 : 여기에 사용된 CRC32는 Linux Kernel의 통신 프로그램 중에서 추출한 것이다.
(7) 패킷 전체 길이 return – 가변길이 이므로 중요하다.
이 패킷을 통신라인을 통해 보내면 다시 나누어 각각의 데이터를 분리하여 사용하면 되는데
char rxBuff[1024];
CPacket *pkt
U16 length;
char *data;
u32 crc;
pkt = (CPacket *) rxBuff;
. . .
length = pkt->length;
crc = pkt-> crc;
. . .
이와 같이 이용하면 되는데 여기서 주의 할 점은 CPU간 데이터 표시가 다르다는데 있다. 위에서 언급한 Big Endian과 Little Endian 문제이다. 만약 2개의 CPU가 각각 다른 endian 방식을 사용한다면 16,32,64 비트의 변수가 MSB와 LSB가 바이트 단위로 바뀌는 문제가 있다.
송신 측에서
pkt->length = length;
해서 보내면
수신 측에서
length = pkt->length;
할 때 2개의 CPU간에 뒤집히는 경우가 있다.
만약 송.수신 측의 길이가 0x0123일 때, 수신에서는 0x2301로 해석된다는 이야기 이다. 이것은 처음 설계부터 고려하여 포맷을 설정하고 통신하고 해석하는 설계를 해야 한다.