/bin/sh: 1: Syntax error: Bad fd number

Posted 2013.01.19 13:24

컴파일을 하다 보니 아래와 같은 메시지가 나오면서 안됨.


/bin/sh: 1: Syntax error: Bad fd number


찾아보니 아래와 같이 하면 된다고 하는데....

sudo rm /bin/sh
sudo ln -s /bin/bash /bin/sh


/bin/sh는 아래와 같이 dash의 소프트 링크이다.


sunny@ubuntu:/$ ll /bin/sh

lrwxrwxrwx 1 root root 4 Dec 26 02:21 /bin/sh -> dash*


저작자 표시 비영리 변경 금지
신고

build는 현재 플랫폼

host는 설치될 플랫폼

target은 실행될 플랫폼을 말한다...


따라서, host와 target은 같다고 보면 됨.

저작자 표시 비영리 변경 금지
신고

fflush(stdin)? __fpurge(stdin)!

Posted 2012.02.10 16:19
간단한 테스트 프로그램을 짜려고 getchar()를 사용하다가 뻘짓만.... ㅡ.ㅡ

while(1)
{
   print_usage();
   choice = getchar();
   switch(choice)
   {
    어쩌고...
       choice2 = getchar();
    저쩌고...
   }
}

위와 같이 만들면 두번째 입력에서 꼭 엔터키가 먹어버리는 현상이 발생한다.

그래서, getchar() 다음에 __fpurge(stdin) 실행하면 원하는 대로 결과를 얻을 수 있다.

아래 방법은 테스트 해보지 않았음.
while (getchar() != '\n') continue;


신고
sqlite로 DB를 구성할 때 다음과 같은 점을 고려

1. 레코드 업데이트 속도는 파일 사이즈에 영향을 덜(?) 받는 거 같다.
2. 레코드 쿼리 속도는 파일 사이즈에 영향을 많이 받는 거 같다.

당연한 건가? ㅡ.ㅡ

신고

pthread_cond_wait

Posted 2011.05.11 10:24
pthread_cond_wait()은 쓰레드가 휴면상태가 되도록 한다. 휴면상태는 pthread_cond_signal()이라는 함수를 통해  깨울 수 있다. pthread_cond_wait()은 두개의 인자가 들어가는데, 첫번째는 조건변수(conditional variable) 이고 두번째는 잠겨진 mutex이다. 조건변수는 pthread_cond_signal()의 인자로 사용되어 진다.

pthread_cond_wait()가 호출되면 내부적으로 mutex를 잠금 해제하고 쓰레드 실행을 중지시킨다. pthread_cond_wat()의 이러한 내부 작동은 언제나 함께 발생(atomic)한다. 이들 사이에 실행되는 쓰레드는 없다.  또 다른 쓰레드가 pthread_cond_signal()을 호출하면 조건 변수를 기다리고 있었던 쓰레드는 깨어난다. 또 다른 쓰레드가 어떤 pthread_cond_broadcast()를 호출하면 모든 쓰레드는 깨어난다.

pthread_cond_wait()에서 깨어날 때 모든 쓰레드가 시도하는 첫 번째 일은 처음 호출될 때 잠금을 해제했던 mutex를 다시 잠그는 것이다.


신고

'Research > Programming' 카테고리의 다른 글

fflush(stdin)? __fpurge(stdin)!  (0) 2012.02.10
sqlite 파일, 테이블, 쿼리, 업데이트  (0) 2011.05.18
pthread_cond_wait  (0) 2011.05.11
Worker Thread in Qt using Signals & Slots  (0) 2011.04.12
An Introduction to SQLite  (0) 2010.02.01
Qt, undefined reference to `vtable for  (1) 2010.01.26

origin URL : http://cdumez.blogspot.com/2011/03/worker-thread-in-qt-using-signals-slots.html

Saturday, March 12, 2011

Worker Thread in Qt using Signals & Slots

Qt provides platform-independent threading classes, a thread-safe way of posting events, and signal-slot connections across threads. Multithreaded programming allows taking advantage of multiprocessor machines and it is also useful to perform time-consuming operations without freezing the user interface of an application.

Without multithreading, all the processing is done in the main (UI) thread.

Signal-slot connections across threads
As mentioned earlier, Qt supports connecting signals and slots across threads. This provides an interesting way to pass data between threads.

Let's have a look at the QObject::connect() method:
?
1
2
3
bool QObject::connect(const QObject *sender, const
char *signal, const QObject *receiver, const char
*method, Qt::ConnectionType type = Qt::AutoConnection);

The last parameter is the connection type and it is important to understand it. The default value is Qt::AutoConnection, meaning that if the signal is emitted from a different thread than the receiving object, the signal is queued, behaving as Qt::QueuedConnection. Otherwise, the slot is invoked directly, behaving as Qt::DirectConnection. The type of connection is determined when the signal is emitted.

In our case, we are particularly interested in Qt::QueuedConnection because:
  • It is safe to use a queued connection between two different threads (direct connection is not)
  • The slot is executed in the receiver's thread. This means that we can emit a signal from the main thread and connect it to a slot in our worker thread. The processing is then done in the worker thread.

Simple example
Let's consider a simple example where we would like to sort a vector of integers in a separate worker thread. As a consequence, we should have two threads: the main (UI) thread and the worker thread that takes care of the sorting.

As shown in the following figure, we need two objects, one living in the main thread (Sorter) and one living in the worker thread (SorterWorker). 
A QObject instance is said to live in the thread in which it is created. Events to that object are dispatched by that thread's event loop.
Visual representation of our threads

Let's have a look at the code of our Sorter class. Because we want to create a new thread, the Sorter class will subclass QThread (which inherits QObject). The Sorter class will provide a sortAsync(QVector) method that will be called by clients to sort a vector asynchronously. Because the operation is asynchronous, we also need to define a vectorSorted(QVector) signal to notify the client when the sorting is done.


Sorter class: header
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <QThread>
#include <QVector>
 
/*! Class for doing work (sorting) in a worker thread */
class Sorter : public QThread {
  Q_OBJECT
 
public:
  /*! Default constructor */
  Sorter(QObject *parent = 0);
 
  /*! Sorts asynchronously a vector in a worker thread */
  void sortAsync(const QVector<int> &list);
 
signals:
  /*!
   * Internal signal used to communicate with
   * the worker thread.
   */
  void sortingRequested(const QVector<int> &list);
  /*! Signal emitted once the vector is sorted */
  void vectorSorted(const QVector<int> &list);
 
protected:
  void run();
 
private:
  /*!
   * Boolean indicating if the worker thread is ready
   * to process requests.
   */
  bool m_ready;
};

Here are some details regarding the implementation of the Sorter class.
In the constructor, we start the worker thread (the code in run() will be executed). Before returning, the constructor waits for the worker thread to be ready (i.e. The SorterWorker object has been created and signals/slots were connected) to make sure that the client cannot make sorting requests before the worker thread is ready to process them.

In the run() method, we create the SorterWorker object. It is important that this object is created inside the run() method and not inside the SorterWorker constructor so that it lives in the worker thread (and not in the main thread). We use an internal sortingRequested(QVector) signal to forward the sorting requests to the SorterWorker in the worker thread. Because the signal is emitted in the main thread and because the receiver (SorterWorker) lives in the worker thread, a queued connection will be used. As a consequence, the doSort(QVector) slot will be executed in the worker thread.
Also note the use of QMetaType::qRegisterMetaType() before connecting our signals/slots. Whenever a signal is queued, the parameters must be of types that are known to Qt's meta-object system, because Qt needs to copy the arguments to store them in an event behind the scenes. If you forget this, you will get the following error:
 QObject.connect: Cannot queue arguments of type 'QList<int>'
 (Make sure 'QList<int>' is registered using qRegisterMetaType().)
Sorter class: source
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <QMetaType>
#include <QDebug>
#include "sorter.h"
#include "sorter_p.h"
 
Sorter::Sorter(QObject *parent):
  QThread(parent), m_ready(false) {
  qDebug() << Q_FUNC_INFO << QThread::currentThreadId(); // Main Thread
  // Start the worker thread
  start();
  // Wait for the worker thread to be ready;
  while(!m_ready) msleep(50);
}
 
void Sorter::sortAsync(const QVector<int> &v)
{
  qDebug() << Q_FUNC_INFO << QThread::currentThreadId(); // Main Thread
  emit sortingRequested(v);
}
 
void Sorter::run()
{
  qDebug() << Q_FUNC_INFO << QThread::currentThreadId(); // Worker Thread
  // This QObject lives in the worker thread
  SorterWorker worker; // DO NOT define 'this' pointer as parent
  // We need to register QList<int> because it is not known
  // to Qt's meta-object system
  qRegisterMetaType< QVector<int> >("QVector<int>");
  // Pass sorting requests to SorterWorker in the worker thread
  connect(this, SIGNAL(sortingRequested(QVector<int>)),
  &worker, SLOT(doSort(QVector<int>))/*, Qt::QueuedConnection*/);
  // Forward the signal to the clients
  connect(&worker, SIGNAL(vectorSorted(QVector<int>)), this,
  SIGNAL(vectorSorted(QVector<int>))/*, Qt::QueuedConnection*/);
  // Mark the worker thread as ready
  m_ready = true;
  // Event loop (necessary to process signals)
  exec();
}
</int>

SorterWorker class
The implementation of the SorterWorker class is straightforward. You just need to make sure that it subclasses QObject (do not forget the Q_OBJECT macro so that signals/slots can be used). Worker methods such as doSort(QVector) have to be defined as public slots. In the case that these methods have to return data, use signals to do so. Do not forget to forward the signals to the clients in the Sorter class.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <QObject>
#include <QVector>
#include <QThread>
#include <QDebug>
 
/*! Class doing the actual work (sorting) */
class SorterWorker: public QObject {
  Q_OBJECT
 
signals:
  /*! Signal emitted once the vector is sorted */
  void vectorSorted(const QVector<int> &v);
 
public slots:
  /*! Method taking care of the actual sorting */
  void doSort(const QVector<int> &v) {
    qDebug() << Q_FUNC_INFO << QThread::currentThreadId(); // Worker Thread
    QVector<int> v_sorted = v;
    qSort(v_sorted);
    emit vectorSorted(v_sorted);
  }
};


How to use our worker thread
To use the worker thread, you simply need to create a Sorter Object and make sure you connect its vectorSorted(QVector) signal to a local slot to get the result. You can then call the sortAsync(QVector) method to ask to worker thread to sort your vector.

?
1
2
3
4
Sorter t;
connect(&t, SIGNAL(vectorSorted(QVector<int>)),
        SLOT(handleVectorSorted(QVector<int>)));
t.sortAsync(QVector<int>() << 1 << 3 << 2);

Debugging
To make sure the functions are executed in the correct thread, you can use the following instruction:
1
qDebug() << Q_FUNC_INFO << QThread::currentThreadId();
QThread::currentThreadId() is a static function that returns the id of the current execution thread.

QtConcurrent alternative
I should mention that for simple cases (such as the one I gave as example), QtConcurrent is a suitable alternative which results in less code.

You can use the following function to run a function is a separate thread:
1
QFuture<T> QtConcurrent::run(Function func, ...);
This executes the function func in a separate thread and returns a QFuture Object. You can check if the function is done executing using QFuture::isFinished() method. Alternatively, you can use a QFutureWatcher object to receive a signal when the function is done.

I hope this helps some of you with multithreaded programming in Qt.

6 comments:

nishant said...

I think We dont need to subclass QThread, and this example can be made very simple. Qt devs have already said that subclassing QThread is not a good idea.

Christophe Dumez said...

From the official QThread documentation:
"To create your own threads, subclass QThread and reimplement run()."

Gareth said...

@Christoper: that statement in the docs dates from before Qt 4.4, when the default implementation of QThread::run() was changed so that it now calls QThread::exec() - and so there is no longer any need to subclass QThread.

This is discussed in the following post on Qt Labs (and in the extensive chain of comments below it).

http://labs.qt.nokia.com/2010/06/17/youre-doing-it-wrong/

nishant said...

as rightly pointed out by Gareth, this is the exact post i was refereing to when i said

"Qt devs have already said that subclassing QThread is not a good idea."

This is an old and wrong way of doing things.

shiroki said...

I used the same implementation as the blogger's for many years. Since this is due to a change introduced in 4.4 and the qt document has not been changed to reflect the new design, it is the document to blame.
http://bugreports.qt.nokia.com/browse/QTBUG-16358

Christophe Dumez said...

I know about this blog article. Please note that I'm not using "moveToThread(this);" in my constructor. I believe that my use of QThread is correct although there may be a simpler solution.

What would be a cleaner solution then? If I try to follow the directions in the Qt blog article, I get the following code: http://pastebin.com/N2V4Vdk2
Frankly, I would not say it is any better. Am I missing something?

신고

'Research > Programming' 카테고리의 다른 글

sqlite 파일, 테이블, 쿼리, 업데이트  (0) 2011.05.18
pthread_cond_wait  (0) 2011.05.11
Worker Thread in Qt using Signals & Slots  (0) 2011.04.12
An Introduction to SQLite  (0) 2010.02.01
Qt, undefined reference to `vtable for  (1) 2010.01.26
Makefile: 변수 출력하기  (0) 2009.12.14

An Introduction to SQLite

Posted 2010.02.01 12:33


An Introduction to SQLite
51:01 - 3년 전
Google TechTalks May 31, 2006 Richard Hipp ABSTRACT SQLite is a small C library that implements a self-contained, embeddable, zero-configuration SQL database engine. SQLite implements a large subset of SQL-92 and stores a complete database in a single disk file. The library footprint is less than 250 KB making is suitable for use in embedded devices and applications where memory space is scarce. This talk provides a quick overview of SQLite, its history, its strengths and weaknesses, and describes situations where it is much more useful than a traditional client/server database. The talk concludes with a discussion of the lessons learned from the development of SQLite and how those lessons can be applied to other projects.
신고

'Research > Programming' 카테고리의 다른 글

pthread_cond_wait  (0) 2011.05.11
Worker Thread in Qt using Signals & Slots  (0) 2011.04.12
An Introduction to SQLite  (0) 2010.02.01
Qt, undefined reference to `vtable for  (1) 2010.01.26
Makefile: 변수 출력하기  (0) 2009.12.14
[SQLITE] INSERT  (0) 2009.11.18

Qt, undefined reference to `vtable for

Posted 2010.01.26 15:21

g++ -Wl,-rpath,/usr/local/Trolltech/Qt-4.6.0/lib -o mythstb version.o main.o globalsettings.o action.o actionset.o mythcontrols.o keybindings.o keygrabber.o livemgrwnd.o chansearch.o channellist.o banner.o setup-language.o setup-avoutput.o svcmgrthread.o moc_mythcontrols.o moc_keygrabber.o moc_livemgrwnd.o moc_chansearch.o moc_channellist.o moc_banner.o moc_setup-language.o moc_setup-avoutput.o    -L/usr/local/Trolltech/Qt-4.6.0/lib -L../../libs/libmyth -L../../libs/libmythdb -L../../libs/libmythui -lmythstb-0.1 -lmythstbui-0.1 -lmythstbdb-0.1 -lfreetype -L/usr/X11R6/lib -lXinerama -lX11 -lXext -lXxf86vm -lXv -lXrandr -L/usr/local/lib -lQtWebKit -L/usr/local/Trolltech/Qt-4.6.0/lib -lQtXmlPatterns -lQtSql -lQtXml -lQtGui -lQtNetwork -lQtCore -lpthread
svcmgrthread.o: In function `ServiceManager':
/work/whs1000-mythstb/programs/mythstb/svcmgrthread.cpp:30: undefined reference to `vtable for ServiceManager'
collect2: ld returned 1 exit status
make[2]: *** [mythstb] 오류 1


컴파일 도중 위와 같은 에러를 만났다. 겉으로 보기에는 아무런 문제가 없어 보인다...


결국 Q_OBJECT 문제임을 알았다.


즉, 헤더에 Q_OBJECT가 선언되면 *.moc 파일을 필요로 하는데, 슬롯과 시그널이 선언되지 않으면 *.moc파일이 안생긴다고 함.


따라서, Q_OBJECT를 선언하지 말거나, 슬롯/시그널을 선언해야 위의 에러를 없앨 수 있다.

신고

'Research > Programming' 카테고리의 다른 글

Worker Thread in Qt using Signals & Slots  (0) 2011.04.12
An Introduction to SQLite  (0) 2010.02.01
Qt, undefined reference to `vtable for  (1) 2010.01.26
Makefile: 변수 출력하기  (0) 2009.12.14
[SQLITE] INSERT  (0) 2009.11.18
gcc 이야기  (0) 2009.07.22

Makefile: 변수 출력하기

Posted 2009.12.14 13:03
8.12 Functions That Control Make

These functions control the way make runs. Generally, they are used to provide information to the user of the makefile or to cause make to stop if some sort of environmental error is detected.

$(error text...)
Generates a fatal error where the message is text. Note that the error is generated whenever this function is evaluated. So, if you put it inside a command script or on the right side of a recursive variable assignment, it won't be evaluated until later. The text will be expanded before the error is generated.

For example,

              ifdef ERROR1
$(error error is $(ERROR1))
endif

will generate a fatal error during the read of the makefile if the make variable ERROR1 is defined. Or,
              ERR = $(error found an error!)

.PHONY: err
err: ; $(ERR)

will generate a fatal error while make is running, if the err target is invoked.

$(warning text...)
This function works similarly to the error function, above, except that make doesn't exit. Instead, text is expanded and the resulting message is displayed, but processing of the makefile continues.

The result of the expansion of this function is the empty string.

$(info text...)
This function does nothing more than print its (expanded) argument(s) to standard output. No makefile name or line number is added. The result of the expansion of this function is the empty string.

신고

'Research > Programming' 카테고리의 다른 글

An Introduction to SQLite  (0) 2010.02.01
Qt, undefined reference to `vtable for  (1) 2010.01.26
Makefile: 변수 출력하기  (0) 2009.12.14
[SQLITE] INSERT  (0) 2009.11.18
gcc 이야기  (0) 2009.07.22
$(warning TEXT...)  (0) 2006.01.09

[SQLITE] INSERT

Posted 2009.11.18 10:37

형식) INSERT INTO table (column_list) VALUES (value_list);


예) INSERT INTO foods (name, type_id) VALUES ('Cinnamon Bobka', 1);

foods 테이블에 name은 'Cinnamon Bobka', type_id는 1인 행을 insert한다.


만약에 value_list에 모든 컬럼의 값들을 제공하면, column_list는 생략가능 하다.

신고

'Research > Programming' 카테고리의 다른 글

Qt, undefined reference to `vtable for  (1) 2010.01.26
Makefile: 변수 출력하기  (0) 2009.12.14
[SQLITE] INSERT  (0) 2009.11.18
gcc 이야기  (0) 2009.07.22
$(warning TEXT...)  (0) 2006.01.09
$(filter PATTERN...,TEXT)  (0) 2006.01.09

gcc 이야기

Posted 2009.07.22 20:13

출처: gcc이야기


gcc는 예전에는 GNU C Compiler의 약자였으나 지금은 GNU Compiler Collection의 약자로 다양한(?) 언어의 컴파일러들의 집합체이다. gcc는 한마디로 GNU에서 개발된 ANSI C 표준을 따르는 C 언어 컴파일러라고 말할 수 있다. gcc는 ANSI C 표준에 따르기는 하지만 ANSI C 표준에는 없는 여러 가지 확장 기능이 있다. 또한 gcc는 통합개발환경(IDE)을 가지고 있지 않은 command line 컴파일러이다. 옛날 Turbo-C를 주로 사용해 보셨던 분들은 tcc.exe와 비슷하다고 생각하면 된다.

(*) -v 옵션
현재 사용되고 있는 gcc의 버전을 나타내는 옵션이다. 특정 소프트웨어 패키지를 컴파일하기 위해 어느 버전 이상의 gcc를 쓰도록 권장하는 경우가 있는데 시스템에 깔려있는 gcc의 버전을 파악하려고 할때 사용한다.

이제 직접 프로그램 하나를 컴파일하면서 설명하도록 하겠다. 아래는 hello.c의 소스이다.

#include〈stdio.h〉

int main()
{
  printf(“hello gccn”);
  return 0;
}

$ gcc -o hello hello.c
로 컴파일하면 실행파일 hello가 만들어진다.

(*) -o 파일이름 옵션
gcc의 수행 결과 파일의 이름을 지정하는 옵션이다. 위의 예제를 단순히
$ gcc hello.c
로 컴파일 하면 hello라고 하는 실행파일이 만들어 지는 것이 아니라 보통의 경우 a.out이라는 이름의 실행파일이 만들어진다.
-o hello 옵션을 줌으로써 결과물을 hello라는 이름의 파일로 만들어지게 하였다.

위의 컴파일 과정을 외부적으로 보기에는 단순히 hello.c파일이 실행파일 hello로 바뀌는 것만 보이지만 내부적으로는 다음과 같은 단계를 거쳐 컴파일이 수행된다.

 (1) C Preprocessing
 (2) C 언어 컴파일
 (3) Assemble
 (4) Linking

C Preprocessing은 C 언어 배울 때 배운 #include, #define, #ifdef 등 #으로 시작하는 여러 가지를 처리해 주는 과정이다. 그 다음 C 언어 컴파일은 C Preprocessing이 끝난 C 소스 코드를 assembly 소스코드로 변환하는 과정이다. Assemble은 그것을 다시 object 코드(기계어)로 변환하고 printf()함수가 포함되어 있는 라이브러리와 linking을 하여 실행파일이 되는 것이다.
위의 네 가지 과정을 모두 gcc라는 실행파일이 해 주는 것일까? 겉으로 보기에는 그렇게 보일지 모르지만 실제로는 gcc는 소위 말해 front-end라고 하여 껍데기에 지나지 않고 각각을 해 주는 다른 실행파일을 gcc가 부르면서 수행된다.
C Preprocessing을 전담하고 있는 실행파일은 cpp라고 하여 /usr/bin 디렉토리와 /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.95.12 디렉토리(당연히 gcc버전과 시스템에 따라 디렉토리 위치가 다르다. gcc -v로 확인하길 바란다.)에 존재한다. C 언어 컴파일은 cc1이라는 실행파일이 담당하는데 /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.95.12 디렉토리에 존재한다. Assemble과 linking은 각각 as와 ld라는 실행파일이 담당하고 /usr/bin 디렉토리에 존재하는 파일이다. (참고 : 시스템에 따라 /usr/bin이 아니라 /bin또는 /usr/local/bin 디렉토리에 존재할 수도 있다.)

gcc라는 실행파일이 하는 일을 정리해 보면 다음과 같다.
 (1) 사용자에게 옵션과 소스 파일명들의 입력을 받는다.
 (2) 소스 파일명의 확장자를 보고 어떤 단계를 처리해야 할지 결정한다.
 (3) 사용자의 옵션을 각각의 단계를 맡고 있는 실행파일의 옵션으로 변경한다.
 (4) 각각의 단계를 맡고 있는 실행파일을 호출(fork, exec)하여 단계를 수행하도록 한다.

=== C Preprocessing(cpp)
C preprocessing을 우리말로 하면 "C 언어 전처리"라고 할 수 있을 것이다. 모든 C 언어 문법책에서 정도의 차이는 있지만 C preprocessing에 대한 내용을 다루고 있다. C preprocessing에 대한 문법은 C 언어 문법의 한 부분으로 가장 기본이 되는 부분이다. C preprocessing에 관한 문법은 모두 '#'으로 시작된다. '#' 앞에는 어떠한 문자(공백 문자 포함)도 오면 안된다. 하지만 대부분의 compiler가 '#'앞에 공백 문자가 오는 경우에도 처리를 한다.

== C preprocessing이 하는 일
 (1) 입력 : C 언어 소스 코드
 (2) 출력 : 전처리가 완료된 C 언어 소스 코드
 (3) 하는 일
  - 파일 포함(file inclusion - 헤더파일 및 기타파일)
  - 매크로(macro) 치환
  - 선택적 컴파일(conditional compile)
  - 기타(#line, #error, #pragma)

cpp 는 C 언어 소스코드를 입력 받아서 C preprocessing에 관련된 문법 사항을 적절히 처리하고 결과로 C 언어 소스코드를 출력하는 프로그램이다. 입력은 작성된 C 언어 소스 코드이고, 출력으로 나온 C 언어 소스 코드에는 C preprocessing 문법에 관련된 어떠한 것도 남아있지 않는다. 즉, #define, #include 등을 찾을 수 없다. 남아 있는 정보가 있다면 file 이름과 줄수(line number)에 관한 정보이다. 그 이유는 추후의 컴파일 과정에서 에러가 날 때 그 정보를 이용해서 error를 리포팅할 수 있도록 하기 위해서이다. 그렇다면 C preprocessing을 직접 해보자.
$ gcc -E -o hello.i hello.c
결과로 hello.i라는 파일이 생긴다. 그 파일을 에디터로 열어보면 hello.c의 첫번째 줄에 있는 #include 를 처리한 결과가 보일것이다.

(*) -E 옵션
-E 옵션은 gcc의 컴파일 과정 중에서 C preprocessing까지만 처리하고 나머지 단계는 처리하지 말라는 것을 지시하는 것이다. 평소에는 별로 쓸모가 있는 옵션이 아니지만 다음과 같은 경우에 유용하게(?) 사용할 수 있다.
 (1) C 언어 소스 코드가 복잡한 선택적 컴파일을 하고 있을 때, 그 선택적 컴파일이 어떻게 일어나고 있는지 알고 싶은 경우.
 (2) preprocessing의 문제가 C 언어 에러로 나타날 경우. 다음과 같은 소스코드를 고려해 보자.

#define max(x, y) ((x) 〉(y) ? (x) : (y) /* 마지막에 ")"가 없다!!! */
int myMax(int a, int b)
{
  return max(a, b);
}

$ gcc -c cpperr.c
다음과 같은 에러가 난다.
cpperr.c: In function `myMax':
cpperr.c:4: parse error before `;'
cpperr.c파일의 4번째 줄에서 ';'가 나오기 전에 parse error가 났다. 하지만 실제 에러는 #define에 있었으므로 그것을 확인하려면 -E 옵션으로 preprocessing을 하여 살펴 보면 쉽게 알 수 있다.

(*) 참고 : parse error before x(어떤 문자) 에러는 소스코드를 parsing 할 때 발생한 에러를 말한다. parsing이란 syntax analysis(구문해석) 과정인데 쉽게 말하면 C 언어 소스코드를 읽어들여 문법적 구성요소들을 분석하는 과정이라고 할 수 있다. 보통 gcc에서 parse error라고 하면 괄호가 맞지 않았거나 아니면 ';'를 빼먹거나 했을 때 발생한다. 보통의 경우 before x라고하여 x라는 것이 나오기 전에 parse error가 발생하였음을 알려주기 때문에 그 x가 나오기 전에 있는 C 소스 코드를 잘 살펴보면 문제를 해결할 수 있다.

C preprocessing의 문법과 나머지 C 언어의 문법과는 거의 관계가 없다. 관계가 있는 부분이 있다면 정의된 macro가 C 언어의 문법 상의 char string literal에는 치환되지 않는다는 정도이다. (좀 더 쉽게 이야기 하면 큰 따옴표 안에서는 macro 치환이 되지 않는다.) 또한 c preprocessing은 architecture dependent하지 않다. 즉, i386용으로 컴파일된 cpp를 다른 architecture에서 사용해도 무방하다. 조금 문제가 있을 수 있는 부분이 있다면 gcc의 predefined macro(i386의 경우 i386용 자동으로 define된다.)가 다를 수 있다는 점 뿐이다. 따라서 cpp를 C 언어 소스코드가 아닌 다른 부분에서 사용하는 경우도 있다. 대표적으로 assembly 소스 코드에서도 사용한다. assembler가 사용하고 있는 macro 문법이 c preprocessing의 macro문법 보다는 배우기 쉽기 때문이다.

이제 preprocessing이 하는 일에 대해서 좀더 알아 보자.
== 파일 포함(file inclusion)
#include 〈stdio.h〉
#include "config.h"
위 와 같이 많은 C 언어 소스코드에서 헤더 파일을 포함한다.〈〉와 ""의 차이는 기본적인 헤더파일과, 사용자 정의 헤더파일을 구분하는 정도이다. include한 헤더 파일은 default로 특정 디렉토리를 찾게 된다. Linux 시스템의 경우 /usr/include가 default 디렉토리이다. (실제로도 그곳에 stdio.h라는 파일이 있다.) 그 다음은 현재 디렉토리를 찾게 된다.(물론〈〉와 ""에 따라서 다르다.) 파일이 없으면 당연히 에러가 발생한다. gcc의 경우 다음과 같은 에러가 발생한다.
>>소스코드파일명:line number: 헤더파일명: No such file or directory
또는(LANG=ko일때)
>>소스코드파일명:line number: 헤더파일명: 그런 파일이나 디렉토리가 없음

그렇다면 include하고 싶은 파일이 default 디렉토리와 현재 디렉토리에 없으면 어떻게 할까? 그런 문제를 해결하기 위해서 다음과 같은 옵션이 존재한다.

(*) -Idir 옵션
여 기서 dir은 디렉토리 이름이고 -I와 디렉토리 이름을 붙여 써야 한다. 그럼 include한 헤더 파일을 그 디렉토리에서도 찾아 주게 된다. 당연히 옵션을 여러 번 다른 디렉토리 이름으로 사용할 수도 있어서 헤더 파일을 찾을 디렉토리를 여러 개로 지정할 수 있다. 꼭 알아 두어야 할 옵션이다.

(*) -nostdinc
이 옵션은 default 디렉토리(standard include 디렉토리)를 찾지말라고 지시하는 옵션이다. 어플리케이션 프로그래머는 관심을 둘 필요가 없지만 kernel 프로그래머는 관심 있게 볼 수 있는 옵션이다.

== macro 치환
macro 치환에 대해서는 특별히 일어날 만한 에러는 없다. 가끔 문제가 되는 부분이 macro 정의가 한 줄을 넘어선 경우 역슬레쉬('')로 이어져야 하는데 그 소스 파일이 windows용 에디터로 편집 되었으면 parse error가 나는 경우가 있다. 그것은 개행문자(new line character)가 서로 달라서 그런 것인데...음 자세히 이야기하자면 끝이 없으므로 그냥 넘어가도록 해야한다. 또한 macro가 define된 상황에서 macro를 undef하지 않고 다시 define하면 다음과 같은 Warning이 난다.
'xxxx' redefined

macro 치환에서 대한 옵션 두개를 알아보도록 하자.

(*) -Dmacro 또는 -Dmacro=defn 옵션
gcc 의 command line에서 macro를 define할 수 있도록 하는 옵션이다. 예를 들어 -D__KERNEL__이라는 옵션을 주면 컴파일 과정 중에 있는 C 언어 소스코드의 맨 처음에 #define __KERNEL__이라고 해준 것과 같이 동작한다. 또한 -DMAXLEN=255라고하면 C 언어 소스코드의 맨 처음에 #define MAXLEN 255 라고 한 것과 동일한 결과를 준다. 선택적 컴파일을 하는 경우에 많이 이용하는 옵션으로 꼭 알아야 할 옵션이다.

(*) -Umacro 옵션
이 옵션은 #undef하고 하는 일이 똑같은데 C 언어 소스코드와는 하등의 관계가 없다. -Dmacro옵션처럼 C 언어 소스코드의 맨처음에 #undef macro를 해주는 것은 아무런 의미가 없기 때문이다.(어짜피 #define은 그 이후에 나올 것이므로...) 이 옵션의 목적은 위의 -Dmacro옵션으로 define된 macro를 다시 undef하고자 할 때 쓰는 옵션이다. 평상시에는 별로 쓸 일이 없는 옵션이지만 그냥 -Dmacro와 같이 짝으로 알아 두길 바란다.

== 선택적 컴파일
#if 시리즈와 #else, #elif, #endif 등으로 선택적 컴파일을 수행할 수 있다. 위에서 설명한 -Dmacro 옵션과 같이 쓰는 경우가 많다. 특별히 설명할 옵션은 없고 #if와 #else, #endif의 짝이 잘 맞아야 한다. 그렇지 않으면 당연히 에러가 발생한다. 단순히 parse error라고 나오는 경우는 드물고, #else, #if 에 어쩌고 하는 에러가 난다. 많이 경우의 수가 있으므로 직접 에러가 발생되도록 코딩을 해보고 확인해 보는 것이 좋다.

== 기타(#line, #error, #pragma)
#line, #error, #pragma라는 것이 있는지도 모르는 사람들이 꽤 있것이다. 자세한 것은 C 언어 문법 책을 찾아보길 바란다. #line의 경우 C 언어 소스코드 직접 쓰이는 경우는 거의 없으니까 무시하고 #pragma는 compiler에 dependent하고 gcc에서 어떤 #pragma를 사용하는지도 알 수 없으므로 그냥 넘어가도록 한다. #error의 경우 C preprocessing 과정에서 강제로 에러를 만드는 지시어이다. 선택적 컴파일 과정에서 도저히 선택되어서는 안 되는 부분에 간혹 쓰인다. 당연히 #error를 만나면 에러가 생긴다. linux kernel 소스코드에서 include 디렉토리를 뒤져 보시면 사용하는 예를 만날 수 있다.

== predefined macro
사 용자가 C 언어 소스코드에서 #define을 하지 않아도 이미 #define된 macro가 있다. ANSI C에서는 __LINE__, __FILE__, __TIME__, __DATE__, __STDC__ 다섯 가지는 이미 define되어 있는 macro로 강제적인 사항이다.(문법책 참조) gcc도 당연히 다섯 가지 macro를 predefine 한다. 뿐만 아니라 GCC의 버전에 대한 macro, architecture에 관한 사항 등을 -Dmacro 옵션 없이도 predefine 한다. -v 옵션을 실행하여 출력되는 specs파일을 열어보면 쉽게 알 수 있을 것이다.(specs파일이 어떻게 해석되는지는 나도 잘 모른다.)

== 꼭 알아두면 좋은 옵션 한가지
다음과 같이 shell 상에 입력해 보라.(hello.c는 계속되는 그 녀석이다.)
$ gcc -M hello.c
어떤 것이 출력되나? "hello.o: hello.c /usr/include/stdio.h 어쩌구저쩌구"가 출력될 것이다. 어디서 많이 본 듯한 형식 아닌가?

(*) -M 옵션
-M 옵션은 cpp에게 makefile로 만들 수 있는 rule을 만들어 달라고 하는 요청을 보내는 명령이다. file을 include하는 녀석은 cpp이므로 rule은 cpp가 만들 수 있다. 당연히 -Dmacro, -Umacro, -Idir 옵션 등을 같이 사용할 수 있고 그에 따라 결과가 달라질 수도 있다. makefile을 좀 쉽고 정확하게 만들 때 쓰는 옵션이므로 알아두면 좋다. 단점은 default 디렉토리에 있는 보통 사용자는 고칠 수도 없는 파일까지도 만들어 준다는 것이다.

=== C 언어 컴파일 과정
C 언어 컴파일 과정은 gcc라고 하는 frontend가 cc1이라는 다른 실행파일을 호출(fork & exec)하여 수행하게 된다. 사용자가 cc1이라는 실행파일을 직접 실행해야 할 하등의 이유도 없고 권장되지도 않는다. gcc의 입력으로 여러 개의 파일(C 소스 파일, object 파일 등)을 준다고 하더라도 컴파일 과정 중 앞 3단계, 즉 cpp, C 컴파일, assemble은 각각의 파일 단위로 수행된다. 서로 다른 파일의 영향을 받지 않고 진행된다. 특정 C소스 코드에서 #define된 macro가 다른 파일에는 당연히 반영되면 안된다. header 파일의 존재 의미를 거기서 찾을 수 있다.

이제 C 언어 컴파일 과정이 하는 일을 알아보도록 하자.

== C 언어 컴파일 과정이 하는 일
 (1) 입력 : C 언어 소스 코드(C preprocessing된)
 (2) 출력 : Assembly 소스 코드
 (3) 하는 일 : 컴파일(너무 간단한가?)

C preprocessing과 마찬가지로 너무 간단하다. 하지만 위의 “컴파일” 과정은 cc1 내부에서는 여러 단계로 나누어져 다음과 같은 순서로 일어난다. Parsing(syntax analysis)이라고 하여 C 언어 소스 코드를 파일로부터 읽어 들여 컴파일러(여기서는 cc1)가 이해하기 쉬운 구조로 바꾸게 된다. 그 다음에 그 구조를 컴파일러가 중간 형태 언어(Intermediate Language)라고 하는 다른 언어로 변환하고 그 중간 형태 언어에 여러가지 최적화를 수행하여 최종 assembly 소스 코드를 만들게 된다.

직접 수행해 보자. 다음과 같이 shell의 command line에 입력하라. 역시 지긋지긋한 hello.c를 이용하도록 한다.
$ gcc -S hello.c
결 과로 출력된 hello.s를 에디터로 열어서 살펴보라 (혹시 위의 command로 hello.s가 만들어 지지 않는다면 gcc -S -o hello.s hello.c로 하라.). “.”으로 시작하는 assembler directive와 “:”로 끝나는 label명, 그리고 몇 개의 assembly mnemonic이 보이나? Assembly 소스를 읽을 줄 몰라도 그게 assembly 소스 코드구나 생각하면 된다.

(*) -S 옵션
-S 옵션은 gcc의 컴파일 과정 중에서 C 언어 컴파일 과정까지만 처리하고 나머지 단계는 처리하지 말라는 것을 지시하는 것이다. 평소에는 거의 사용하지 않는 옵션이지만 다음과 같은 경우에 유용하게 사용할 수 있다.
 (1) 어셈블리 코드가 어떻게 생겼는지 보고 싶은 호기심이 발동한 경우
 (2) C calling convention을 알아보거나 stack frame이 어떻게 관리되고 있는 지 보고 싶은 경우

보 통의 경우는 아니지만 사용자가 직접 assembly 코딩을 하는 경우가 종종 있다. 아무래도 사람이 기계보다는 훨씬 똑똑하기 때문에 사람이 직접 assembly 코딩을 해서 최적화를 시도하여 소프트웨어의 수행 시간을 단축시키거나, 아니면 linux kernel이나 bootloader 등과 같이 꼭 assembly가 필요한 경우가 있다. 이때도 보통의 경우는 소프트웨어의 전 부분을 assembly 코딩하는 것이 아니라 특정 부분만 assembly 코딩을 하고 나머지는 C 언어나 다른 high-level 프로그래밍 언어를 써서 서로 연동을 하도록 한다. 그럼 C 언어에서 assembly 코딩된 함수를 호출할 때(반대의 경우도 마찬가지), 함수의 argument는 어떻게 전달되는 지, 함수의 return 값은 어떻게 돌려지는지 등을 알아볼 필요가 있다. 이렇게 argument와 return 값의 전달은 CPU architecture마다 다르고 그것을 일정한 약속(convention)에 따라서 처리해 주게 된다. 위의 hello.s를 i386용 gcc로 만들었다면 파일 중간에 xorl %eax,%eax라는 것이 보일 것이다. 자기 자신과 exclusive or를 수행하면 0(zero)이 되는데 이것이 바로 hello.c에서 return 0를 assembly 코드로 바꾼 것이다. 결국 i386 gcc에서는 %eax 레지스터에 return 값을 넣는다는 convention이 있는 것이다.(실제로는 gcc뿐 아니라 i386의 convention으로 convention을 따르는 모든 compiler가 %eax 레지스터를 이용하여 return값을 되돌린다.) argument의 경우도 test용 C 소스를 만들어서 살펴볼 수 있을 것이다. 물론 해당 CPU architecture의 assembly 소스코드를 어느 정도 읽을 수 있는 사람들에게만 해당하는 이야기 이다. stack frame도 비슷한 얘기 쯤으로 알아 두길 바란다.

== Parsing(Syntax Analysis)
위 에서 cc1이 컴파일을 수행하는 과정 중에 맨 첫 과정으로 나온 Parsing에 대해서는 좀더 언급을 한다. Parsing과정은 그야말로 구문(Syntax)을 분석(Analysis)하는 과정이다. Parsing의 과정은 파일의 선두에서 뒤쪽으로 한번 읽어 가며 수행된다. Parsing 중에 컴파일러는 구문의 에러를 찾는 일과 뒤에 수행될 과정을 위해서 C 언어 소스 코드를 내부적으로 다루기 쉬운 형태(보통은 tree형식을 이용)로 가공하는 일을 수행한다. 이 중에 구문의 에러를 찾는 과정은 (1) 괄호 열고 닫기, 세미콜론(;) 기타 등등의 문법 사항을 체크하는 것 뿐만 아니라, (2) identifier(쉽게 말해 변수나 함수 이름 들)의 type을 체크해야 한다.
 (1) 괄호 열고 닫기, 세미콜론(;) 기타 등등의 문법 사항에 문제가 생겼을 때 발생할 수 있는 에러가 전에 이야기한 parse error이다. 보통 다음과 같이 발생한다.
>> 파일명과 line number: parse error before x
당연히 에러를 없애려면 ‘x’ 앞 부분에서 괄호, 세미콜론(;) 등을 눈 빠지게 보면서 에러를 찾아 없애야 한다.
 (2) type checking
구문 에러를 살필 때 type 체크를 왜 해야 할까? 다음과 같은 예를 보자.
var3 = var1 + var2;
앞 뒤에서 parse error가 없다면 위의 C 언어 expression은 문법적인 문제가 없는가? 하지만 var1이 파일의 앞에서 다음과 같이 정의(definition)되었다면 어떻게 될까?
struct point { int x; int y; } var1;
당연히 ‘+’ 연산을 수행할 수 없다.(C 언어 문법상) 결국은 에러가 난다. 이렇게 identifier(여기서는 var1, var2, var3)들의 type을 체크하지 않고서는 구문의 에러를 모두 찾아낼 수 없다.
만 약 var1과 var3가 파일의 앞에서 int var1, var3;로 정의되어 있고 var2가 파일의 앞에 어떠한 선언(declaration)도 없이 파일의 뒤에서 int var2;로 정의되어 있다면 에러가 발생할까? 정답은 “발생한다”이다. 위에서 언급했듯이 Parsing은 파일의 선두에서 뒤쪽으로 한번만(!!!) 읽으면서 진행하기 때문이다.(모든 C 컴파일러가 그렇게 동작할지는 의심스럽지만 ANSI C 표준에서는 그렇게 되어 있는 것으로 알고 있다. Assembler는 다르다.)
그 렇다면 어떤 identifier를 사용하려면 반드시 파일 중에 사용하는 곳 전에 identifier의 선언(declaration) 또는 정의(definition)가 있어야 한다. 하지만 identifier가 함수 이름일 경우(즉 identifier뒤에 (…)가 올 경우)는 조금 다르다. C 컴파일러는 함수 이름 identifier의 경우는 int를 return한다고 가정하고 Error가 아닌 Warning만 출력한다.(Warning옵션에 따라 Warning조차 출력되지 않을 때도 있다.) 그럼 다음과 같은 소스 코드를 생각해 보자.
int var3, var2;
….
var3 = var1() + var2;
….
struct point var1(void) { … }
위 와 같은 경우도 문제가 생긴다. 맨 처음 var1이라는 함수 이름 identifier를 만났을 때 var1 함수는 int를 return한다고 가정했는데 실제로는 struct point를 return하므로 에러 또는 경고를 발생한다.
결국 권장하는 것은 모든 identifier는 사용하기 전(파일 위치상)에 선언이나 정의를 해 주는 것이다. 다음과 같은 에러 메시지들을 짧막하게 설명해 본다.
 파일명 line number: ‘x’ undeclared …. 에러 --> ‘x’라는 이름의 identifier가 선언되어 있지 않았다.
  파일명 line number: warning: implicit declaration of function `x' … 경고 --> ‘x’라는 이름의 함수가 선언되어 있지 않아 int를 return한다고 가정했다는 경고(Warning) 메시지이다.

변수나 함수의 선언(declaration)과 정의(definition)에 대해서 알지 못한다면 C 언어 문법책을 찾아서 숙지하길 바란다. 그런 내용이 없다면 그 문법책을 휴지통에 버리길 바란다.

Parsing 과정에는 위의 identifier 에러 및 경고를 비롯한 수많은 종류의 에러와 경고 등이 출력될 수 있다. 에러는 당연히 잡아야 하고 경고도 무시하지 않고 찾아서 없애는 것이 좋은 코딩 습관이라고 할 수 있다. 경고 메시지에 대한 gcc 옵션을 살펴보도록 하자.

(*) -W로 시작하는 거의 모든 옵션
이 옵션들은 어떤 상황 속에서 경고 메시지를 내거나 내지 말라고 하는 옵션이다. -W로 시작하는 가장 강력한 옵션은 -Wall 옵션으로 모든 경고 메시지를 출력하도록 한다. 보통은 -Wall 옵션을 주고 컴파일 하는 것이 좋은 코딩 습관이다.

== Parsing 이후 과정
특 별한 경우가 아닌 이상 Parsing을 정상적으로 error나 warning없이 통과한 C 소스 코드는 문법적으로 완벽하다고 봐야 한다. 물론 논리적인 버그는 있을 수 있지만 이후 linking이 되기 전까지의 과정에서 특별한 error나 warning이 나면 안된다. 그런 경우가 있다면 이제는 사용자의 잘못이 아니라 gcc의 문제로 추정해도 무방하다. Parsing이후에 assembly 소스 코드가 생성되는데, 당연히 이 과정에는 특별히 언급할 만한 error나 warning은 없다. 그냥 중요한 옵션 몇 가지만 집고 넘어가도록 하겠다.

(*) -O, -O2, -O3 등의 옵션
이 옵션은 컴파일러 최적화를 수행하라는 옵션이다. -O 뒤의 숫자가 올라갈수록 더욱 많은 종류의 최적화를 수행하게 된다. 최적화를 수행하면 당연히 코드 사이즈도 줄어 들고 속도도 빨라지게 된다. 대신 컴파일 수행 시간은 길어진다. 그리고 linux kernel을 위해 언급하고 싶은 것은 inline 함수들은 이 옵션을 주어야 제대로 inline 된다는 것이다.

(*) -g 옵션
이 옵션은 소스 레벨 debugger인 gdb를 사용하기 위해 debugging 정보(파일명, line number, 변수와 함수 이름들과 type 등)를 assembly code와 같이 생성하라는 옵션이다. 당연히 gdb를 이용하고 싶으면 주어야 한다. -g 옵션을 주지 않고 컴파일한 프로그램을 gdb로 디버깅하면 C 소스 레벨이 아닌 assembly 레벨 디버깅이 된다. 즉 C 소스 코드에 존재하는 변수 이름, line number 등이 없는 상황에서 디버깅을 해야 한다. 또한 -g 옵션을 -O 옵션과 같이 사용할 수도 있다. 단 그런 경우 최적화 결과, C 소스 코드에 존재하는 심볼(symbol; 쉽게 말해 함수와 변수 이름)중에 없어지는 것들이 발생한다.

(*) 여기서 잠깐
identifier 와 symbol이 모두 “쉽게 말해 함수와 변수 이름”이라고 했는데 어떻게 차이가 날까? 엄밀히 말하면 차이가 조금 있다. symbol이 바로 “쉽게 말해 함수와 변수 이름”이며 각 symbol은 특정 type과 연계되어 있다. 하지만 identifier는 그냥 “이름” 또는 “인식어”일 뿐이다. 예를 들어 struct point { int x; int y; };라는 것이 있을 때 point는 symbol은 아니지만 identifier이다. 보통 identifier라는 말은 parsing에서만 쓰인다는 정도만 알아두면 된다.

(*) -p 옵션과 -pg 옵션
profiling 을 아는가? 수행시간이 매우 중요한 프로그램(real time 프로그램이라고 해도 무방할 듯)을 작성할 때는 프로그램의 수행 시간을 함수 단위로 알아야 할 필요가 있는 경우가 많다. 프로그램의 수행 시간을 함수 단위나 더 작은 단위로 알아보는 과정을 profiling이라고 하는데, profiling은 프로그램 최적화에 있어서 중요한 기능을 담당한다. 대부분의 개발 툴이 지원하고 Visual C++에도 존재한다. 옛날 turbo C에는 있었나? 아무튼 gcc도 역시 profiling을 지원한다. -p 옵션 또는 -pg 옵션을 주면 프로그램의 수행 결과를 특정 파일에 저장하는 코드를 생성해 주게 된다. 그 특정 파일을 적당한 툴(prof또는 gprof 등)로 분석하면 profiling 결과를 알 수 있게 해 준다. 당연히 linux kernel 등에서는 사용할 수 없다.(이유는 특정 파일에 저장이 안되므로…) 초보자들은 이런 옵션도 존재하고 profiling을 할 수 있다는 정도만 알아 두면 좋을 듯 싶다. 나중에 필요하면 좀 더 공부해서 사용하면 된다.

(*) 기타 옵션(-m과 -f시리즈)
중 요한 옵션들이기는 하지만 초보자가 알아둘 필요가 없는 옵션 중에 f또는 m으로 시작하는 옵션들이 있다. f로 시작되는 옵션은 여러 가지 최적화와 assembly 코드 생성에 영향을 주는 architecture independent한 옵션이다.(assembly 코드 생성이 architecture dependent 이므로 정확히 말하면 f로 시작되는 옵션이 architecture independent라고 할 수는 없다.) m으로 시작되는 옵션은 보통 architecture dependent 하며 주로 CPU의 종류를 결정하는 옵션으로 assembly 코드 생성에 영향을 주게 된다. 하지만 대부분은 초보자는 그런 것이 있다는 정도만 알아두면 되고 특별히 신경 쓸 필요는 없다고 생각된다. m으로 시작되는 옵션 중에 -msoft-float옵션이 있다.(물론 특정 architecture에만 존재하는 옵션이다.) -msoft-float 옵션은 CPU에 FPU(floating point unit)가 없고, kernel에서 floating-point emulation을 해 주지 않을 때 C 소스 코드 상에 있는 모든 floating-point 연산을 특정 함수 호출로 대신 처리하도록 assembly 코드를 생성하라고 지시하는 옵션이다. 이 옵션을 주고 라이브러리를 linking시키면 FPU가 없는 CPU에서도 floating 연산을 할 수 있다.(대신 엄청 느리다. 어찌보면 kernel floating-point emulation보다는 빠를 것 같은데 확실하지는 않다.)

=== Assemble 과정
Assemble 과정은 앞선 과정과 동일하게 gcc라는 frontend가 as라는 실행 파일을 호출하여 수행된다. 그런데 as는 cpp와 cc1과는 달리 gcc 패키지 안에 존재하는 것이 아니라 별도의 binutils라고 하는 패키지에 존재한다. binutils 패키지 안에는 as를 비롯해 linking을 수행하는 ld, library 파일을 만드는 ar, object 파일을 보거나 복사할 수 있는 objdump, objcopy 등 여러 가지 툴이 들어 있다.

이제 Assemble 과정이 하는 일을 알아보도록 하자.

== Assemble 과정이 하는 일
 (1) 입력 : Assembly 소스 코드
 (2) 출력 : relocatable object 코드
 (3) 하는 일 : assemble(너무 간단한가?)

입 력은 당연히 C 언어 컴파일 과정을 거치면 나오는 Assembly 소스 코드이다. Assemble 과정을 거치면 소위 기계어(machine language)라는 결과가 relocatable object 형식으로 나온다. “relocatable”이라는 말이 어려우면 그냥 object 코드라고 해 두자. 이제 직접 수행해자. shell의 command line에 다음과 같이 입력하면 된다.
$ gcc -c hello.c
결 과는 hello.o라고 하는 파일이 나온다. hello.o는 binary형식의 파일이니깐 editor로 열어봐야 정보를 얻기 힘들다. 당연히 위의 예는 assemble 과정만 수행한 것이 아니라 C preprocessing 과정, C 언어 컴파일 과정, Assemble 과정을 수행했다. Assemble 과정만 수행하고 싶으면 다음과 같이 입력하면 된다.
$ gcc -c hello.s
역시 hello.o가 생긴다. hello.s는 C 언어 컴파일 과정에서 -S 옵션으로 만들었던 그 파일이다. 별로 관심이 안 생기면 as를 직접 수행할 수도 있다. 다음과 같다.
$ as -o hello.o hello.s
역시 hello.o가 생긴다.

(*) -c 옵션
많 이 쓰는 옵션이다. Assemble 과정까지의 과정만 수행하고 linking 과정을 수행하지 말라는 옵션이다. 여러 개의 C 소스 파일로 이루어진 프로그램을 컴파일 할 때 모든 소스 파일을 assemble 과정까지 수행하고 맨 마지막에 linking한다. 보통은 Makefile을 많이 이용하는데 그 때 많이 쓰이는 옵션이다.

Assemble 과정에서는 더 이상 기억해야 하는 옵션도 없고 이게 끝이다. C 언어 컴파일 과정에서 말한 바대로 C 언어 컴파일 과정이 끝난 C 소스 파일은 문법적으로 완전하다고 볼 수 있으므로 assemble 과정에서 Error나 Warning 나는 경우는 없다. 만약 Error나 Warning이 나는 경우가 있다면 gcc의 inline assemble을 이용했을 때, 그 inline assemble 소스 코드에 있는 문제 때문에 생길 수 있다. 안타깝지만 error나 warning 메시지가 나온 다면 C 소스 파일과 line number 정보는 없다. 잘 알아서 처리하는 수 밖에 다른 방법은 없는 것 같다. inline assemble 같은 것을 사용하지 않았는데도 error나 warning이 난다면 gcc의 버그라고 생각해도 무방하다.

== relocatable object 코드 파일 내용
어 떤 정보가 object 파일 안에 들어있을까? 당연히 code와 data가 들어 있다. C 컴파일 과정에서 C 언어 함수 안에 있는 내용들이 assembly mnemonic 들로 바뀌었고 그것이 assemble되어 기계어(machine language)가 되었을 것이다. 그 부분이 code를 이룬다. C 언어 소스 코드에 있는 나머지는 전역 변수(external variable)와 정적 변수(static variable)들이 data를 이룰 것이다. 또한 문자열 상수를 비롯한 상수도 data에 들어 있다. 또한 프로그램 수행에 쓰이지는 않고 단순한 정보로서 들어 있는 data들도 있다. 예를 들어 -g 옵션을 주고 컴파일 하면 프로그램의 디버깅 정보(변수, 함수 이름, C 소스 파일이름, line number 등)가 data에 속한다고 볼 수 있다. 그런데 code와 data가 무질서하게 섞여 있는 것은 아니고 section이라고 불리우는 단위로 서로 구분되어 저장되어 있다. Code는 text section에 들어 있고, data는 성격에 따라 data section, bss section, rodata section 등에 나누어져 저장되어 있다.(text, data, bss, rodata 등의 section 이름은 그냥 관습적인 것이다.) 아무튼 section 이야기는 이 정도만 우선 알아두면 될 듯 싶다.

== Symbol 이야기
relocatable object code안에 code와 data가 들어 있다고 했는데, 아주 중요한 것을 빠뜨렸다. 이 이야기는 linking 과정을 이해하기 위해 꼭 필요한 부분이므로 반드시 읽어야 할 것이다. 우선 Symbol이 무엇인지 알 것이다. C 언어 컴파일 과정에서 identifier와 함께 설명했는데 잠시 다시 말씀하자면 Symbol은 함수와 변수 이름이다. 변수 중에 특히 관심두어야 할 것 들은 자동 변수(?,auto variable)들이 아닌 전역 변수(external variable)와 정적 변수(static variable)이다. 자동 변수는 함수의 stack frame에 존재하는 변수이기 때문에 현재 stack pointer(sp, 보통의 CPU의 register중에 하나)에 대한 offset으로 표현된다. 즉 현재 함수에서 자동 변수(auto variable)를 access(read/write)하고 싶으면 sp+상수의 어드레스를 access하면 된다. 하지만 전역 변수와 정적 변수는 그냥 32bit(32bit CPU기준) 어드레스를 읽어야 한다. stack pointer랑은 전혀 관계 없다. 아무튼 여기서 관심을 두는 Symbol은 함수, 전역 변수와 정적 변수의 이름이라고 할 수 있다.
이제 생각해 볼 것은 C 언어 소스 파일을 C preprocessing, C 언어 컴파일, assemble 과정을 거치면 완전한 기계어로 바꿀 수 있느냐 하는 점이다. 완전히 기계어로 바꿀 수 있을까? C 언어 소스 파일 하나로 이루어지는 프로그램이라면 완전히 기계어로 바꾸는 것이 가능하겠지만 일반적으로는 불가능 하다. 다음과 같은 예제를 살펴보자.
int func3(void); /* func3 선언 */
extern int mydata; /* mydata 선언 */

int func2(void) /* func2 정의 */
{
….
}

int func1(void) /* func1 정의 */
{
int i;
…..
func2();
…..
func3();
….
i= mydata+3;
…..
}
-- end of test1.c
-- start of test2.c
int mydata = 3; /* mydata 정의 */
int func3(void) /* func3 정의 */
{
…..
}

위 의 예제를 컴파일 한다고 생각해보자. test1.c에서 func1()의 내용을 기계어로 바꾸고 싶은데 func2()를 호출하는 시점에서는 별로 문제가 안된다. func2()는 같은 소스 코드 내에 존재하고 func2()를 호출하는 instruction과 func2()의 실제 위치(어드레스)의 차이를 계산해 낼 수 있으므로 상대 어드레스를 이용하는 함수 호출 instruction으로 완전히 기계어로 바꿀 수 있다. 그런데 문제는 func3()를 호출할 때는 func3()의 실제 위치(address)를 계산할 수 없다는 문제점이 있다. 당연히 동일한 파일에 존재하는 함수가 아니므로 그 함수가 존재하게 될 어드레스를 계산할 수 없다. 어드레스를 모르는데 함수 호출 instruction을 완전히 만들 수 있을까? 만들 수 없다. 당연히 전역 변수 mydata를 access하는 부분도 마찬가지로 mydata의 어드레스를 모르므로 완전히 instruction으로 바꿀 수 없다. 그럼 어떻게 해야 될까?
그때 assembler는 그냥 함수 어드레스 없는 함수 호출 instruction을 기계어로 바꾸어 놓는다. 그런 다음에 그 instruction에 “func3()를 호출한다”라는 표지를 붙여 놓는다. 그럼 그 후의 과정(linking)에서 func3()의 address를 계산했을 때 그 빈 공간을 채워 넣게 된다. mydata와 같은 전역 변수도 마찬가지로 동작한다. test1.c을 컴파일할 때는 “func3()”, “mydata” 라는 표지를 사용해야 한다. 그럼 test2.c를 컴파일 할 때는 무엇이 필요할까? 상식적으로 생각하면 “func3()”, “mydata”가 여기 있다라는 정보를 가지고 있어야한다.
정리하면 object 파일 안에는 그 object 파일에 들어있는 symbol들(test1.o에서는 func1과 func2, test2.o에서는 func3와 mydata)에 대한 정보가 들어있고, 그 object 파일이 reference하고 있는 symbol들(test1.o에서 func3와 mydata 사용)에 대한 정보가 들어 있다.

== Relocatable의 의미
위 에서 object 코드라고 하지 않고 relocatable object 코드라고 지칭했는데 relocatable이 뜻하는 것을 잠시 집고 넘어 가자. Relocatable을 사전에서 찾아보면 “재배치가 가능한” 정도의 뜻이다. “재배치가 가능한” 이라는 의미는 상당히 모호하다. 좀 더 구체적으로 말하자면 위에서 설명된 symbol들의 절대 어드레스가 정해지지 않았다는 뜻이다. 즉 test1.c의 func1()이 절대 어드레스 0x80000000에 존재해야 한다라고 정해지지 않고 어떤 절대 어드레스에 존재해도 관계 없다는 뜻이다. 그런데 이 말과 헷갈리는 말이 한가지 더 있는데 그것은 position independent code이다. C 언어 컴파일 과정에서 설명한 옵션중에 -f 시리즈가 있었다. 그 중에 -fpic라는 position independent code를 만들라고 강제하는 옵션이 있다. position independent code도 역시 절대 어드레스상에 어느 위치에 있어도 무방한 code를 지칭한다. 하지만 두 가지는 분명 차이가 있는데, 그냥 넘어가기로 하자. 쉽게 relocatable은 절대 어드레스가 결정되지 않았다는 뜻, 그러나 position independent code와는 다른 말이다.

=== Linking 과정
Linking 과정은 ld라고 하는 실행파일이 담당하고 있다. Assemble을 담당하는 as와 마찬가지로 binutils 패키지의 일부분이다. 보통 어플리케이션을 컴파일하는 경우에는 gcc(실행파일)를 이용하여 ld를 호출하나, 특별한 경우에 있어서는 ld를 직접 수행하여 linking을 하는 경우가 종종 있다.

== Linking 과정이 하는 일
 (1) 입력 : 하나 이상의 relocatable object 코드 와 library
 (2) 출력 : 실행파일(executable) 또는 relocatable object 코드
 (3) 하는 일 : symbol reference resolving & location
Linking 과정은 하나 또는 그 이상의 object 파일과 그에 따른 library를 입력으로 받는다. 출력은 보통의 경우는 실행파일(executable file)이지만, 경우에 따라서 object 파일을 생성하게 할 수도 있다. 여러 개의 object 파일을 합쳐서 하나의 object 파일로 만드는 과정을 partial linking이라고 부르기도 한다. Linking 과정이 하는 일은 symbol reference resolving하고 location이라고 했는데, 저도 정확한 단어를 적은 것인지 의심스럽다. 정확한 용어를 사용한다면 좋겠지만 그렇지 못하더라도 내용을 정확히 이해하는 것이 중요하니깐 내용에 대해서 살펴보도록 하겠다.

== symbol reference resolving
어 떤 C 소스 파일에서 다른 파일에 있는 함수와 전역 변수(symbol)에 대한 참조(reference)를 하고 있다면 assemble 과정에서 완전한 기계어로 바꿀 수 없다.(실제로는 같은 소스 파일에 있는 전역 변수를 참조하는 것도 보통의 경우, 완전한 기계어로 바꿀 수 없다.) 그 이유는 당연히 assemble 까지의 과정은 단일 파일에 대해서만 진행되고, 다른 파일에 있는 해당 함수와 전역 변수의 address가 상대적이든 절대적이든 결정될 수 없기 때문이다. 따라서 완전히 기계어로 바꿀 수 없는 부분은 그대로 “공란”으로 남겨두고 표시만 해 두게 된다.
Linking 과정에서 그 “공란”을 채워 넣게 된다. 그 과정을 보통 “resolve한다”라고 말한다. 어떻게 할까? 당연히 실행 파일을 이루는 모든 object 파일을 입력으로 받기 때문에 object 파일들을 차곡 차곡 쌓아 나가면(아래 location 참조) object 파일 안에 있는 모든 symbol(함수나 전역 변수 이름)의 address를 상대적이든 절대적이든 계산할 수 있다. 이제 각 symbol의 address가 계산되었으므로 표시가 남아 있는 “공란”에 해당하는 symbol의 address를 잘 넣어주면 된다.
linking 과정에서 나올 수 있는 에러는 대부분 여기에서 발생한다. 표시가 남아 있는 “공란”을 채울 수 없는 경우가 있다. 크게 두 가지로 나누어지는데, 우선 reference하고 있는 symbol을 찾을 수 없는 경우와 reference하고 있는 symbol의 정의가 여러 군데에 있는 경우이다.

>> object파일명: In function ‘func’:
>> object파일명: undefined reference to ‘symbolname’
위 의 에러 메시지는 함수 func 안에서 사용되고 있는 symbolname이란 이름의 symbol이 어디에도 정의되지 않아서 “공란”을 채울 수 없다는 뜻이다. 당연히 symbolname을 잘못 입력하였던지 아니면 그 symbol이 속해있는 object 파일이나 library와 linking되지 않았기 때문이다.

>> object파일명1: multiple definition of ‘symbolname’
>> object파일명2: first defined here
위 의 에러 메시지는 symbolname이란 이름의 symbol이 여러 번 정의되고 있다는 뜻이다. object파일1에서 정의가 있는데 이미 object파일2에서 정의된 symbol이므로 그 symbol을 reference하고 있는 곳에서 정확하게 “공란”을 채울 수 없다는 뜻이다. 당연히 두 symbol중에 하나는 없애거나 static으로 바꾸거나 해야 해결될 것이다.

== location(용어 정확하지 않을 수 있음)
이 전 까지 object 코드를 모두 relocatable이라고 표현했다. 아직 절대 address가 결정되지 않았다는 의미로 사용된다.(position independent code와는 다른 의미) object 코드의 절대 address를 결정하는 과정이 “location”이다. Symbol reference resolving 과정에서 입력으로 받은 모든 object 파일들을 차곡 차곡 쌓아 나간다고 했다. 그런데 object 파일이 무슨 벽돌도 아닌데 차곡 차곡 쌓는 다는 것이 말이 되나? 여기서 쌓는 다는 말을 이해하기 위해서 다음과 같은 그림(?)을 살펴 보도록 하자.

많은 object code들
----------------- address(0xAAAAAAAA+0x5000)
test2.o(size 0x3000)
----------------- address(0xAAAAAAAA+0x2000)
test1.o(size 0x2000)
----------------- address(0xAAAAAAAA)

절 대 address 0xAAAAAAAA에 test1.o의 내용을 가져다 놓는다. test1.o의 크기(파일 크기와는 의미가 조금 다르지만 그냥 무시하고 파일 크기라고 생각하기 바람)가 0x2000이므로 다음에 test2.o를 쌓을 수 있는 address는 0xAAAAAAAA+0x2000가 된다. 그곳에 다시 test2.o를 쌓고 또 test2.o의 크기를 보고 새로운 address 계산하고 또 object 코드 쌓고, 계속 반복이다. 이렇게 쌓을 때 초기 절대 address 0xAAAAAAAA가 무슨 값을 가지게 되면 모든 object 파일에 있는 symbol의 절대 address도 계산해 나갈 수 있을 것이다. 그걸로 symbol reference를 resolve하게 된다. 그 초기 절대 address 0xAAAAAAAA의 값을 정하는 것을 location이라고 한다. 그럼 왜 절대 address를 결정해야 할까? 꼭 그래야 할 필요는 없지만 CPU의 instruction이 대부분의 경우 절대 address를 필요로 하는 경우가 많기 때문이라고 할 수 있다.
(주의) object 를 쌓는 것은 위의 예처럼 단순하지는 않다. 실제로는 object 전체를 쌓지 않고 object안에 있는 section별로 쌓게 된다.

그럼 이제 직접 수행해 보자.
$ gcc -o hello hello.o
object 파일이 하나라서 너무 단순하다고 생각하는가? 물론 hello.o 하나만 command line에 나타나지만 실제로는 조금 많은 object 파일이 linking되고 있다. (아래에서 좀더 자세하게 설명한다.) 지겹지만 hello를 실행해 보라. 제대로 동작하는가? 제대로 동작한다면 그 사이 어떤 일이 벌어졌을까? 그 사이에 벌어진 일을 간단히 적어보면 다음과 같다. shell이 fork() 시스템콜을 호출하고 자식 process는 exec() 시스템콜을 통해 hello라는 파일 이름을 kernel에 넘긴다. kernel에서는 hello파일을 보고 linking할 때 location된 address(여기서는 absolute virtual address 이다.)상의 메모리로 hello 파일을 복사하고 PC(program counter)값을 바꾸면 수행되기 시작한다.
(주의) 실제로 위의 hello가 수행되는 과정은 많은 생략과 누락이 있다. 실제로는 hello 파일을 완전히 메모리로 복사하는 것도 아니고, dynamic linking & loading 등의 개념이 완전히 빠져 있지만 그냥 이해하기 쉽게 하기 위해서 간단하게 적어 본 것이다.

= library
hello.o 를 linking하여 hello라고 하는 실행파일을 만드는데 command line에서는 아무것도 없지만 library가 같이 linking되고 있다. 그것은 지극히 당연하다. hello.c의 main함수에서 printf함수를 호출(linking이니깐 참조 혹은 reference라고 해야 좋겠다.)하고 있는데 printf함수 자체는 소스 중에 그 어디에도 없다.(물론 stdio.h에 printf함수의 선언은 있습니다만 정의는 어디에도 없다.) 잘 알다시피 printf함수는 C standard library 안에 있는 함수이다. C standard library가 같이 linking되었기 때문에 제대로 동작하는 hello 실행파일이 생긴 것이다.
library라는 것은 아주 간단한 것이다. relocatable object 파일들을 모아 놓은 파일이다. 소스로 제공할 수도 있으나, 그러면 매번 cpp, c 컴파일, assemble 과정을 거쳐야 하므로 컴파일 시간이 매우 증가하게 된다. 그래서 그냥 relocatable object 파일로 제공하는 것이 컴파일 시간 단축을 위해서 좋다. 그런데 필요한 relocatable object 파일이 너무 많으면 귀찮으니까 그것을 묶어서 저장해 놓은 녀석이 바로 library라고 할 수 있다.
Linux를 비롯한 unix 계열에서는 대부분의 library 파일의 이름이 lib로 시작된다. 확장자는 두 가지가 있는데, 하나는 .a이고 또 하나는 .so입니다.(뒤에 library 버전 번호가 붙는 경우가 많다.) .a로 끝나는 library를 보통 archive형식의 library라고 말하며 .so로 끝나는 library를 보통 shared object라고 부른다. /lib 디렉토리와 /usr/lib 디렉토리에 가면 많이 볼 수 있다.
archive library 안에 있는 symbol를 reference하게 되면 library중에 해당 부분(object 파일 단위)을 실행 파일 안에 포함시켜 linking을 수행한다. 즉, 해당 object 파일을 가지고 linking을 수행하는 것과 동일한 결과를 가진다. 보통 이런 linking을 static linking이라고 부른다.
그 런데 시스템 전체에 현재 수행되고 있는 실행파일(실행파일이 수행되고 있는 하나의 단위를 process라고 한다.)들에서 printf함수를 사용하고 있는 녀석들이 매우 많으므로 그것이 모두 실행 파일에 포함되어 있다면 그것은 심각한 메모리 낭비를 가져온다는 문제점을 가지고 있다. 그래서 생각해 낸 것이 dynamic linking이라는 개념이다. 예를 들어 실행파일이 printf함수를 사용한다면 실행파일이 메모리로 loading될 때 printf가 포함되어 있는 library가 메모리 상에 있는 지 검사를 해 보고 있으면 reference resolving만 수행하고, 아니라면 새로 loading과 reference resolving을 하게 된다. 그렇게 되면 printf가 포함되어 있는 library는 메모리 상에 딱 하나만 loading되면 되고 메모리 낭비를 막을 수 있다. 그런 일을 할 수 있도록 도입된 것이 shared object 이다. MS Windows쪽의 프로그래밍을 하시는 사람이라면 DLL과 동일한 개념이라고 보면 된다.
그런 shared object를 이용하여 dynamic linking을 하면 실행파일의 크기가 줄어든다. 반면에 당연히 실행파일이 메모리에 loading될 때는 reference resolving을 위해서 CPU의 연산력을 사용한다. 하지만 MS Windows의 DLL과는 달리 shared object 파일과 static linking을 할 수도 있다.(반대로 archive library를 이용하여 dynamic linking을 수행할 수는 없다.)

(*) -static 옵션
dynamic linking을 지원하고 있는 시스템에서 dynamic linking을 수행하지 않고 static linking을 수행하라는 옵션이다. dynamic linking을 지원하고 있는 시스템에서는 dynamic linking이 default 이다.

직접 수행해 보자.
$ gcc -o hello_static -static hello.o
실행파일 hello, hello_static 을 수행하면 결과는 똑같다. 파일의 크기를 비교해 보면 차이가 난다는 것을 알 수 있을 것이다.

/lib, /usr/lib에는 엄청 많은 library 파일들이 존재한다. 그럼 linker가 찾아야 하는 symbol을 모든 library 파일에 대해서 검사를 하는 것일까? CPU하고 HDD가 워낙 빠르면 그래도 무방하겠지만, 그렇게 하지 않는다.(“사용자가 쉽게 할 수 있는 일을 컴퓨터에게 시키지 말라.”라는 컴퓨터 사용 원칙이다.) 우선 gcc는 기본적인 library만 같이 linking을 하게 되어 있다. 나머지 library는 사용자의 요구가 있을 때만 같이 linking을 시도하도록 되어 있다. 그럼 기본적인 library가 무엇인지 알아야 하고 gcc에게 사용자의 요구를 전달할 옵션을 있어야 할 것이다. 기본적인 library는 당연히 C standard library 이다. C standard library의 이름은 libc.a또는 libc.so 이다. 최근의 linux 머신은 /lib/libc.so.6 이라는 파일을 찾아 볼 수 있을 것이다 (symbolic link되어 있는 파일이다). 그리고 libgcc라고 하는 것이 있는데… 생략하고. 이제 옵션을 알아보자.

(*) -nostdlib 옵션
이 름에서 의미하는 바대로 standard library를 사용하지 말고 linking을 수행하라는 뜻이다. 실제로는 standard library 뿐 아니라 startup file이란 녀석도 포함하지 않고 linking이 수행된다. startup file에 대해서는 좀 있다가 알아보도록 하겠다.

(*) -l라이브러리이름 옵션
특 정 이름의 library를 포함하여 linking을 수행하라는 뜻이다. 예를 들어 -lmyarchive라고 하면 libmyarchive.a(또는 libmyarchive.so)라는 library파일과 같이 linking을 수행하는 것이다. library 파일 이름은 기본적으로 lib로 시작하니깐 그것을 빼고 지정하도록 되어 있다.

library에 대해서 또 하나의 옵션을 알아야 할 필요가 있다. 다름 아닌 “어느 디렉토리에서 library를 찾는가”이다. 모든 library가 /lib와 /usr/lib에 있으라는 보장이 없다. 그 디렉토리를 정하는 방법은 두 가지 인데 LD_LIBRARY_PATH라고 하는 이름의 환경 변수를 셋팅하는 방법이 있고 또 한 가지는 gcc의 옵션으로 넘겨 주는 방법이 있다.

(*) -Ldir 옵션
library 파일을 찾는 디렉토리에 “dir”이란 디렉토리를 추가하라는 옵션이다.(-Idir 옵션처럼 -L과 dir을 붙여서 적습니다.) 예를 들어 -L/usr/local/mylib 라고 하면 /usr/local/mylib라는 디렉토리에서 library 파일을 찾을 수 있게 된다.

== entry 이야기
application 을 작성하고 compile, linking 과정이 지나면 실행 파일이 만들어진다. 그리고 그 실행 파일이 수행될 때는 메모리로 load되어 수행이 시작된다는 사실을 알고 있다. 여기서 한가지 의문이 생기는데, “과연 코드의 어떤 부분에서 수행이 시작되는가?”이다. 답이 너무 뻔한가? main함수부터 수행된다고 답할 것인가? 다소 충격적이겠지만 “땡”이다. main함수부터 수행되지 않고 그전에 수행되는 코드가 존재한다. 그 먼저 수행되는 코드에서 하는 일은 여러 가지가 있는데 그냥 건너 뛰도록 하겠다. 아무튼 그 코드에서 main함수를 호출해 주고 main함수가 return하면 exit 시스템호출을 불러 준다. 그래서 main이 맨 처음 수행되는 것처럼 보이고 main이 return하면 프로그램 수행이 종료되는 것이다. 그럼 그 코드는 어디 있을까? 시스템에 따라서 다르겠지만 일반적으로 /lib혹은 /usr/lib 디렉토리에 crt1.o라는 이름의 object 파일이 있는데 그 object 파일 안에 있는 _start라는 이름의 함수(?)가 맨 먼저 수행되는 녀석이다. 결국 보통 application의 entry는 _start함수가 된다.
그럼 crt1.o object 파일 역시 같이 linking 되어야 한다. gcc를 이용해 linking을 수행할 때 command line에 아무 이야기를 해주지 않아도 자동으로 crt1.o 파일이 함께 linking 된다. 실제로는 crt1.o 뿐 아니라 비슷한 crt*.o 파일들도 같이 linking 된다. 그렇게 같이 linking 되고 있는 object파일들을 startup file이라고 부르는 것 같다.(-nostdlib 옵션 설명할 때 잠시 나왔던 startup file이 바로 이 녀석들이다.) 그럼 ld는 _start파일이 entry인지 어떻게 알고, 다른 이름의 함수를 entry로 할 수는 없는것일까? 그것에 대한 해답은 아래 linking script부분에서 해결될 것이다.

== 실행 파일에 남아 있는 정보
linking 의 결과 실행파일이 생겼는데, 보통 linux에서는 실행파일 형식이 ELF라는 포멧을 가진다.(linux 시스템에 따라 다를 수 있다.) ELF는 Executable and Linkable Format의 약자이다. 보통 linux 시스템에서의 relocatable object 파일의 형식도 ELF이다. 실제로 실행파일과 relocatable object 파일과는 조금 다른 형식을 가진다. 암튼 그건 상식으로 알아두고, 그럼 실행파일에 있는 정보는 무엇일까?
이제까지의 알아낸 정보들을 모두 종합하면 알 수 있다. 우선 실행 파일이라는 녀석이 결국은 relocatable object 를 여러 개 쌓아놓은 녀석이므로 원래 relocatable object 파일이 가지고 있던 code와 data 정보는 모두 남아있을 것이다. 그리고 entry를 나타내는 address가 있어야 수행을 할 수 있을 것이다. 또, dynamic linking을 했을 경우 관련된 shared object 정보도 남아있어야 한다.
실행 파일 속에 남아있는 data는 relocatable object에 있는 data처럼 프로그램 수행에 필요한 data가 있고 그냥 실행 파일을 설명하는 정보로서의 data가 있다. 예를 들어 -g 옵션을 주고 컴파일한 실행파일에서 디버깅 정보들은 실행과는 전혀 관계 없다. 따라서 그러한 정보들은 실행 파일 수행시에 메모리에 load될 필요도 없다.(load하면 메모리 낭비니깐) 실행 파일 속에 남아있는 code와 data는 relocatable object 처럼 특별한 단위로 저장되어 있다. ELF 표준에서는 segment라고 부르는데 보통의 경우는 object 파일처럼 section이라는 말이 쓰인다. reloctable object 파일과 마찬가지로 code는 text section에 저장되고 프로그램 수행 중에 필요한 data가 성격에 따라 나누어져 data, rodata, bss section이란 이름으로 저장되어 있다. 그 section단위로 메모리로 load될 필요가 있는지에 대한 flag정보가 있고 각 section이 load될 address(location과정에서 정해진다.)가 적혀 있어야 정확하게 loading을 할 수 있다.
기타로 symbol reference resolving이 끝났는데도 ELF형식의 실행파일은 보통의 경우 많은 symbol 정보를 그냥 가지고 있는 경우가 있다. symbol 정보 역시 수행에는 하등 관계가 없으므로 없애도 되는데, strip이라고 하는 binutils안에 있는 tool로 없앨 수 있다.

== linking script
흠 이제 좀 어려운 이야기를 할 차례이다. Location과정에서 어떤 절대 address를 기준으로 각 section들을 쌓는지, 그리고 entry는 어떤 symbol인지에 대한 정보를 linker에게 알려줄 필요가 있다. 보통 application의 경우는 시스템 마다 표준(?, 예를 들어 entry는 _start 다 하는 식)이 있는지라 별로 문제될 것은 없는데, bootloader나 kernel을 만들 때는 그런 정보를 사용자가 넘겨 주어야 할 필요가 있다. 그런 것들을 ld의 command line argument로 넘길 수도 있지만 보통의 경우는 linking script라고 하는 텍스트 형식의 파일 안에 저장하여 그 script를 참조하라고 알려준다. (아무래도 command line argument로 넘겨 줄 수 있는 정보가 한계가 있기 때문이라고 생각이 든다. location과 entry에 관한 내용 중에 ld의 command line argument로 줄 수 있는 옵션이 몇가지 있으나 한계가 있다.) ld의 옵션 -T으로 linking script 파일 이름을 넘겨 주게 된다.(gcc의 옵션 아님) linux kernel source를 가지고 있는 사람은 arch/*/*.lds 파일을 한번 열어 보길 바란다. 그게 linking script고, 초기 절대 address 하고 section 별로 어떻게 쌓으라는 지시어와 entry, 실행 파일의 형식 등을 적어 놓은 내용이 보일 것이다. 물론 한 줄 한 줄 해석이 된다면 이런 글을 읽을 필요가 없다. 그 script를 한 줄 한 줄 정확히 해석해 내려면 GNU ld manual 등을 읽어야 할 것이다.

== linux의 insmod
linux kernel을 구성하고 device driver 등은 linux kernel module(이하 module) 형식으로 run-time에 올릴 수 있다는 것을 알고 있을 것이다. module을 run-time에 kernel에 넣기 위해서 사용하는 명령어가 insmod이다.(modprobe도 가능) 이 module 이라는 것이 만들어 지는 과정을 잘 살펴 보면 gcc의 옵션중에 -c옵션으로 컴파일만 한다는 것을 알 수 있다. 확장자는 .o를 사용한다. relocatable object 파일이고, ELF형식이다. 그럼 이 module이 linux kernel과 어떻게 합쳐질까? 당연히 linking 과정을 거쳐야 된다. 일종의 run-time linking 이다. 당연히 module은 kernel내의 많은 함수와 전역 변수를 참조한다. 그렇지 않다면 그 module은 linux kernel의 동작과는 전혀 관계 없는 의미 없는 module이 될것이다. 그럼 참조되고 있는 symbol을 resolving하기 위해서는 symbol의 절대 address를 알아야한다 . 그 내용은 linux kernel 내부에 table로 존재한다. /proc/ksyms라고 하는 파일을 cat 해보면 절대 address와 symbol 이름을 살펴볼 수 있을 것이다. 살펴보면 알겠지만 생각보다 적은 양이다. 적은 이유는 그 table이 linux kernel source에 있는 전역 symbol의 전부를 포함한 것이 아니라 kernel source 내부나 module 내부에서 EXPORT_SYMBOL()과 같은 특별한 방법으로 선언된(?, 이 선언은 C 언어 문법의 declaration과는 다르다.) symbol들만 포함하기 때문이다. 다른 전역 symbol 들은 module 프로그래밍에 별 필요가 없다고 생각되어 지는 녀석들이기 때문에 빠진 것이다. 따라서 EXPORT_SYMBOL()등으로 선언된 symbol들만 사용하여 module을 작성해야 한다. 당연히 linking 과정을 거치기 때문에 앞서 설명한 linking에서 발생할 수 있는 에러들이 발생할 수 있다. 제일 많이 발생할 수 있는 것은 역시 undefined reference 에러이다. gcc의 에러와는 조금 다른 메시지가 나오겠지만 결국은 같은 내용이다.

== map 파일
linking 과정을 끝내면 당연히 모든 symbol에 대한 절대 address가 정해지게 된다. 그 정보를 알면 프로그램 디버깅에 도움이 될 수도 있으니 알면 좋을 것이다. ld의 옵션중에 '-Map 파일이름'이라는 옵션이 있는데 우리가 원하는 정보를 문서 파일 형식으로 만들어 준다. 그 파일을 보통 map 파일이라고 부른다. symbol과 address 정보 말고 section에 대한 정보도 있고 많은 정보가 들어 있다.
linux kernel을 컴파일을 하고 나면 나오는 결과 중에 System.map이라는 파일이 있는데 이 녀석이 바로 ld가 만들어 준 map 파일의 내용 중에 symbol과 symbol의 절대 address가 적혀 있는 파일이다. linux kernel panic으로 특정 address에서 kernel이 죽었다는 메시지가 console에 나오면 이 System.map 파일을 열어서 어떤 함수에서 죽었는지 알아볼 수도 있다.

== 옵션 넘기기
gcc 의 이야기 맨 처음에 gcc는 단순히 frontend로 command line으로 받은 옵션을 각 단계를 담당하고 있는 tool로 적절한 처리를 하여 넘겨준다고 말했었다. 위에서 나온 ld의 옵션 -T와 -Map 과 같은 옵션은 gcc에는 대응하는 옵션이 존재하지 않는다. 이런 경우 직접 ld를 실행할 수도 있고 gcc에게 이런 옵션을 ld에게 넘겨 주라고 요청할 수 있다. 하지만 application을 컴파일할 때는 ld를 직접 실행하는 것은 조금 부담이 되므로, gcc에 옵션을 넘기라고 요청하는 방법이 조금 쉽다고 볼 수 있다. 그런 경우 사용되는 것이 -Wl 옵션인데 간단히 이용해 보도록 하자.
$ gcc -o hello -static -Wl,-Map,hello.map hello.c
그럼 hello.map이라는 매우 큰 문서 파일이 만들어진다. 한번 살펴 보도록 하자.(-static 옵션을 안 넣으면 살펴볼 내용이 별로 없을까봐 추가했다.)
실제로는 -Wl 옵션처럼 as에게도 옵션을 넘겨 줄 수 있는 -Wa와 같은 옵션이 있는데 쓰는 사람을 본 적이 없다.
신고

'Research > Programming' 카테고리의 다른 글

Makefile: 변수 출력하기  (0) 2009.12.14
[SQLITE] INSERT  (0) 2009.11.18
gcc 이야기  (0) 2009.07.22
$(warning TEXT...)  (0) 2006.01.09
$(filter PATTERN...,TEXT)  (0) 2006.01.09
외부 프로그램을 실행시키고 출력결과를 가져오려면 ?  (0) 2005.08.23

$(warning TEXT...)

Posted 2006.01.09 13:53
$(warning TEXT...)

This function works similarly to the error function, above, except that make
doesn’t exit. Instead, TEXT is expanded and the resulting message is displayed,
but processing of the makefile continues.
The result of the expansion of this function is the empty string.
신고

'Research > Programming' 카테고리의 다른 글

[SQLITE] INSERT  (0) 2009.11.18
gcc 이야기  (0) 2009.07.22
$(warning TEXT...)  (0) 2006.01.09
$(filter PATTERN...,TEXT)  (0) 2006.01.09
외부 프로그램을 실행시키고 출력결과를 가져오려면 ?  (0) 2005.08.23
Learn a new trick with the offsetof() macro  (0) 2004.12.31

$(filter PATTERN...,TEXT)

Posted 2006.01.09 13:48
$(filter PATTERN...,TEXT)

Returns all whitespace-separated words in TEXT that *do* match any of the
PATTERN words, removing any words that *do not* match. The patterns are written
using %, just like the patterns used in the patsubst function above.
The filter function can be used to separate out different types of strings (such
as file names) in a variable. For example:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
says that foo depends of foo.c, bar.c, baz.s and ugh.h but only foo.c,
bar.c and baz.s should be specified in the command to the compiler.
신고
http://www.joinc.co.kr/modules/moniwiki/wiki.php/FAQ?action=recall&rev=1.27#toc

1.4 외부 프로그램을 실행시키고 출력결과를 가져오려면 ? #

  • 제가 만든 프로그램에서 'ls'등을 실행시키고 화면에 출력되는 값들을 받아 오려면 어떻게 해야 하는지 궁금합니다. 이 값들을 읽어들이고 분석해서 어떤 일을 하는 프로그램을 짜고 싶습니다.
fork()시킨후에 execl를 이용해서 외부 명령어를 실행시키고 이것을 pipe로 연결하는 방법이 있습니다. 그러나 이것은 복잡한 방법이고 간단하게 popen()을 사용하면 됩니다.
#include <stdio.h>
int main()
{
    FILE *fp = NULL;
    char buff[256];
    if ((fp = popen("ls -al", "r")) == NULL)
    {
        perror("popen error ");
        exit(0);
    }
    while(fgets(buff, 255, fp))
    {
        printf("%s", buff);
    }
    fclose(fp);
}

신고
http://www.embedded.com/shared/printableArticle.jhtml?articleID=18312031
Learn a new trick with the offsetof() macro

Embedded.com   

Learn a new trick with the offsetof() macro
By Nigel Jones, Courtesy of Embedded Systems Programming
3?11 2004 (15:00 $?
URL: http://www.embedded.com/showArticle.jhtml?articleID=18312031

Click here for reader response to this article

Almost never used, the offsetof() macro can actually be a helpful addition to your bag of tricks. Here are a couple of places in embedded systems where the macro is indispensable—packing data structures and describing how EEPROM data are stored.

If you browse through an ANSI C compiler's header files, you'll come across a very strange looking macro in stddef.h. The macro, offsetof(), has a horrid declaration. Furthermore, if you consult the compiler manuals, you'll find an unhelpful explanation that reads something like this:

"The offsetof() macro returns the offset of the element name within the struct or union composite. This provides a portable method to determine the offset."

At this point, your eyes start to glaze over, and you move on to something that's more understandable and useful. Indeed, this was my position until about a year ago when the macro's usefulness finally dawned on me. I now kick myself for not realizing the benefits earlier—the macro could have saved me a lot of grief over the years. However, I console myself by realizing that I wasn't alone, since I'd never seen this macro used in any embedded code. Offline and online searches confirmed that offsetof() is essentially not used. I even found compilers that had not bothered to define it. How the macro works

Before delving into the three areas where I've found the macro useful, it's necessary to discuss what the macro does, and how it does it.

The offsetof() macro is an ANSI-required macro that should be found in stddef.h. Simply put, the offsetof() macro returns the number of bytes of offset before a particular element of a struct or union.

The declaration of the macro varies from vendor to vendor and depends upon the processor architecture. Browsing through the compilers on my computer, I found the example declarations shown in Listing 1. As you can see, the definition of the macro can get complicated.

Listing 1: A representative set of offsetof() macro definitions

// Keil 8051 compiler
#define offsetof(s,m) (size_t)&(((s *)0)->m)

// Microsoft x86 compiler (version 7)
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)

// Diab Coldfire compiler
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))

Regardless of the implementation, the offsetof() macro takes two parameters. The first parameter is the structure name; the second, the name of the structure element. (I apologize for using a term as vague as "structure name." I'll refine this shortly.) A straightforward use of the macro is shown in Listing 2.

Listing 2: A straightforward use of offsetof()

typedef struct
{

    int i;
    float f;
    char c;
} SFOO;

void main(void)
{
  printf("Offset of 'f' is %u", offsetof(SFOO, f));
}

To better understand the magic of the offsetof() macro, consider the details of Keil's definition. The various operators within the macro are evaluated in an order such that the following steps are performed:

  1. ((s *)0) takes the integer zero and casts it as a pointer to s.
  2. ((s *)0)->m dereferences that pointer to point to structure member m.
  3. &(((s *)0)->m) computes the address of m.
  4. (size_t)&(((s *)0)->m) casts the result to an appropriate data type.

By definition, the structure itself resides at address 0. It follows that the address of the field pointed to (Step 3 above) must be the offset, in bytes, from the start of the structure. At this point, we can make several observations:

  • We can be a bit more specific about the term "structure name." In a nutshell, if the structure name you use, call it s, results in a valid C expression when written as (s *)0->m, you can use s in the offsetof() macro. The examples shown in Listings 3 and 4 will help clarify that point.
  • The member expression, m, can be of arbitrary complexity. Indeed, if you have nested structures, then the member field can be an expression that resolves to a parameter deeply nested within a structure.
  • It's easy enough to see why this macro also works with unions.
  • The macro won't work with bitfields. You simply can't take the address of a bitfield member of a structure or union.

Listings 3 and 4 contain simple variations on the usage of this macro. These should help you get you comfortable with the offsetof() basics.

Listing 3: Without a typedef

struct sfoo {

    int i;
    float f;
    char c;
};

void main(void)
{
  printf("Offset of 'f' is %u", offsetof(struct sfoo, f));
}

Listing 4: Nested structs

typedef struct
{

    long l;
    short s;
} SBAR;

typedef struct
{

    int i;
    float f;
    SBAR b;
} SFOO;

void main(void)
{
  printf("Offset of 'l' is %u",     offsetof(SFOO, b.l));
}

Now that you understand the semantics of the macro, it's time to take a look at how to use it.

Use 1: pad bytes
Most 16-bit and larger processors require that data structures in memory be aligned on a multibyte (for example, 16-bit or 32-bit) boundary. Sometimes the requirement is absolute, and sometimes it's merely recommended for optimal bus throughput. In the latter case, the flexibility is offered because the designers recognized that you may wish to trade off memory access time with other competing issues such as memory size and the ability to transfer (perhaps via a communications link or direct memory access) the memory contents directly to another processor that has a different alignment requirement.

For cases such as these, it's often necessary to resort to compiler directives to achieve the required level of packing. As the C structure declarations can be quite complex, working out how to achieve this can be daunting. Furthermore, after poring over the compiler manuals, I'm always left with a slight sense of unease about whether I've really achieved what I set out to do.

The most straightforward solution to this problem is to write a small piece of test code. For instance, consider the moderately complex declaration given in Listing 5.

Listing 5: A union containing a struct

typedef union
{

    int i;
    float f;
    char c;
struct {
    float g;
    double h;
  } b;
} UFOO;

void main(void)
{
  printf("Offset of 'h' is %u",     offsetof(UFOO, b.h)); }

If you need to know where b.h resides in the structure, then the simplest way to find out is to write some test code such as that shown in Listing 5. This is all well and good, but what about portability? Writing code that relies on offsets into structures can be risky—particularly if the code gets ported to a new target at a later date. Adding a comment is of course a good idea—but what one really needs is a means of forcing the compiler to tell you if the critical members of a structure are in the wrong place. Fortunately, one can do this using the offsetof macro and the technique in Listing 6, courtesy of Michael Barr.

Listing 6: Anonymous union that checks structure offsets

typedef union
{

    int i;
    float f;
    char c;
    struct    
    {    
       float g;
       double h;
    } b;    

} UFOO;

static union
{
        char wrong_offset_i[offsetof(UFOO,i) == 0];
        char wrong_offset_f[offsetof(UFOO,f) == 0];

        char wrong_offset_h[offsetof(UFOO, b.h) == 2]; //Error generated
};

The technique works by attempting to declare an array. If the test evaluates to FALSE, then the array will be of zero size, and a compiler error will result. In Listing 6, the compiler generates an error "Invalid dimension size [0]" for the parameter b.h.

Thus the offsetof() macro allows you to both determine and check the packing of elements within structures.

Use 2: nonvolatile memory
Many embedded systems contain some form of nonvolatile memory, which holds configuration parameters and other device-specific information. One of the most common types of nonvolatile memory is serial EEPROM. Normally, such memories are byte addressable. The result is often a serial EEPROM driver that provides an API that includes a read function that looks like this:

ee_rd(uint16_t offset, uint16_t nBytes, uint8_t * dest)

In other words, read nBytes from offset offset in the EEPROM and store them at dest. The problem is knowing what offset in EEPROM to read from and how many bytes to read (in other words, the underlying size of the variable being read). The most common solution to this problem is to declare a data structure that represents the EEPROM and then declare a pointer to that struct and assign it to address 0x0000000. This technique is shown in Listing 7.

Listing 7: Accessing parameters in serial EEPROM via a pointer

typedef struct
{

    int i;
    float f;
    char c;

} EEPROM;

EEPROM * const pEE = 0x0000000;

ee_rd(&(pEE->f), sizeof(pEE->f), dest);

This technique has been in use for years. However, I dislike it precisely because it does create an actual pointer to a variable that supposedly resides at address 0. In my experience, this can create a number of problems including:

  1. Someone maintaining the code can get confused into thinking that the EEPROM data structure really does exist.
  2. You can write perfectly legal code (for example, pEE->f = 3.2) and get no compiler warnings that what you're doing is disastrous.
  3. The code doesn't describe the underlying hardware well.

A far better approach is to use the offsetof() macro. An example is shown in Listing 8

Listing 8: Using offsetof()to access parameters in serial EEPROM

typedef struct
{

    int i;
    float f;
    char c;
} EEPROM;

ee_rd(offsetof(EEPROM,f), 4, dest);

However, there's still a bit of a problem. The size of the parameter has been entered manually (in this case "4"). It would be a lot better if we could have the compiler work out the size of the parameter as well. No problem, you say, just use the sizeof() operator. However, the sizeof() operator doesn't work the way we would like it to. That is, we cannot write sizeof(EEPROM.f) because EEPROM is a data type and not a variable.

Now normally, one always has at least one instance of a data type so that this is not a problem. In our case, the data type EEPROM is nothing more than a template for describing how data are stored in the serial EEPROM. So, how can we use the sizeof() operator in this case? Well, we can simply use the same technique used to define the offsetof() macro. Consider the definition:

#define SIZEOF(s,m) ((size_t) sizeof(((s *)0)->m))

This looks a lot like the offsetof() definitions in Listing 1. We take the value 0 and cast it to "pointer to s." This gives us a variable to point to. We then point to the member we want and apply the regular sizeof() operator. The net result is that we can get the size of any member of a typedef without having to actually declare a variable of that data type.

Thus, we can now refine our read from the serial EEPROM as follows:

ee_rd(offsetof(EEPROM, f), SIZEOF(EEPROM, f), &dest);

At this point, we're using two macros in the function call, with both macros taking the same two parameters. This leads to an obvious refinement that cuts down on typing and errors:

#define EE_RD(M,D)   ee_rd(offsetof(EEPROM,M), SIZEOF(EEPROM,M), D)

Now our call to the EEPROM driver becomes much more intuitive:

EE_RD(f, &dest);

That is, read f from the EEPROM and store its contents at location dest. The location and size of the parameter is handled automatically by the compiler, resulting in a clean, robust interface.

Use 3: protecting nonvolatile memory
The last example contains elements from the first two examples. Many embedded systems contain directly addressable nonvolatile memory, such as battery-backed SRAM. It's usually important to detect if the contents of this memory have been corrupted. I usually group the data into a structure, compute a CRC (cyclic redundancy code) over that structure, and append it to the data structure. Thus, I often end up with something like this:

struct nv
{

    short param_1;
    float param_2;
    char param_3;
    uint16_t crc;
} nvram;

The intent of the CRC is that it be the CRC of "all the parameters in the data structure with the exception of itself." This seems reasonable enough. Thus, the question is, how does one compute the CRC? If we assume we have a function, crc16( ), that computes the CRC-16 over an array of bytes, then we might be tempted to use the following:

nvram.crc = crc16((char *) &nvram, sizeof(nvram)-sizeof(nvram.crc));

This code will only definitely work with compilers that pack all data on byte boundaries. For compilers that don't do this, the code will almost certainly fail. To see that this is the case, let's look at this example structure for a compiler that aligns everything on a 32-bit boundary. The effective structure could look like that in Listing 9.

Listing 9: An example structure for a compiler that aligns everything on a 32-bit boundary

struct nv
{

    short param_1; //Offset = 0
    char pad1[2]; //Two byte pad
    float param_2; //Offset = 4
    char param_3; //Offset = 8
    char pad2[3]; //Three byte pad
    uint16_t crc; //Offset = 12
    char pad3[2]; //Two byte pad

} nvram;

The first two pads are expected. However, why is the compiler adding two bytes onto the end of the structure? It does this because it has to handle the case when you declare an array of such structures. Arrays are required to be contiguous in memory, too. So to meet this requirement and to maintain alignment, the compiler pads the structure out as shown.

On this basis, we can see that the sizeof(nvram) is 16 bytes. Now our nave code in Listing 9 computes the CRC over sizeof(nvram) - sizeof(nvram.crc) bytes = 16 - 2 = 14 bytes. Thus the stored CRC now includes its previous value in its computation! We certainly haven't achieved what we set out to do.

Listing 10: Nested data structures

struct nv
{
    struct data
    {

       short param_1; //Offset = 0
       float param_2; //Offset = 4
       char param_3; //Offset = 8

    } data;
       uint 16_t crc; //Offset = 12
} nvram;

The most common solution to this problem is to nest data structures as shown in Listing 10. Now we can compute the CRC using:

nvram.crc = crc16((uint8_t *) &nvram.data, sizeof(nvram.data));

This works well and is indeed the technique I've used over the years. However, it introduces an extra level of nesting within the structure—purely to overcome an artifact of the compiler. Another alternative is to place the CRC at the top of the structure. This overcomes most of the problems but feels unnatural to many people. On the basis that structures should always reflect the underlying system, a technique that doesn't rely on artifice is preferable—and that technique is to use the offsetof() macro.

Using the offsetof() macro, one can simply use the following (assuming the original structure definition):

nvram.crc = crc16((uint8_t *) &nvram, offsetof(struct nv, crc));

Keep looking
I've provided a few examples where the offsetof() macro can improve your code. I'm sure that I'll find further uses over the next few years. If you've found additional uses for the macro I would be interested to hear about them.

Nigel Jones is a consultant living in Maryland, where he slaves away on a wide range of embedded projects. He enjoys hearing from readers and can be reached at najones@rmbconsulting.us.



Reader Response

Well whadya know! In my last large firmware project, I rolled my own offsetof() macro that looked like this:

//This macro is for hiding the tortured expression needed to extract
//the offset of the elements of the PAGE_TRAILER structure:
#define OFFSET_OF(element) ((char*)&(((PAGE_TRAILER*)0)->element) - (char*)0)

Had no idea it was provided in the standard library!

The IAR compiler which I use implements it like this:

#define offsetof(type,member) ((size_t)(&((type *)0)->member))

Even though it is in the STDDEF.H header file, it's not mentioned in the IAR documentation. Maybe that's why I missed it!

Thanks for pointing it out!

Brad Peeters
President
Theta Engineering


I enjoyed your article and would like to mention another use of the offsetof() macro. In the past I've found the offsetof() macro quite useful in providing an abstract interface, without having to use an object orientated high-level language (e.g. C++). For example, in the past I created a library function to calculate the standard deviation of a set of floats, through the use of a function with the following prototype:

  float stdDev(void *start,
    size_t stride,
    size_t offset,
    size_t num);

The value of start is typically the starting address of a static array of structures, that contains collected data. The sizeof of array entry is provided by the stride parameter. While how many there are is given by num. The value of num is typically obtained through the use of:

  #define NUM(_a) (sizeof(_a)
    / sizeof(_a[0]))

While the offset of the float type in the structure is given by the offset parameter. The nice thing about this function is that it has no idea of the structure type that it is dealing with. All it knows is that some specified number of bytes from the start of the structure is a value of type float.

I've also used similar techniques where the data is contained on a linked list or binary tree. If the data is on a linked list, instead of providing the stride and number of entries, the offset to the next pointer member can be provided. A common practice is to always have the next pointer at the begining of the structure. In this case a value of 0 is implied for the offset to the pointer to the next node. Sometimes the data I deal with is linked on multiple lists. When there are multiple lists, there will be multiple next pointers, usually with similar but always different member names. A function that takes the offset of the next pointer is very useful, in that it can work with structures that are linked onto multiple lists.

I made quick reference to also using this technique on binary trees. In this case the offset of the parent, left child, and right child nodes is needed. It tends to be awkward to pass the offset of these 3 pointers, plus the offset of the data to be processed as parameters. A solution to this issue is to maintain a structure that describes the entire tree. When the tree is created, a structure is created that contains the following:

  struct {
    struct treeAttributes {
      size_t parentOffset;
      size_t leftChildOffset;
      size_t rightChildOffset;
      size_t nodeSize;
      struct *root;
    };
  }

For this technique, it is agreed upon all the trees that use the generic functions, to always have a treeAttribute structure as the first member. With this, it's now possible to create functions such as:

  float stddevTree(void *tree,
    size_t floatOffset);

In this case, the tree parameter points to a structure of an unknown type, but it's agreed that its first member will be of type treeAttribute.

In the example, treeAttribute structure, I've also provided the size of each node. This wouldn't be needed to calculate the standard deviation, but is quite useful with other generic functions. For example, it can be used to automatically create duplicates of nodes with a specified attribute. These nodes might be written to a caller specified file descriptor, as a means to write selected nodes out to a file.

Louis Huemiller

Copyright 2003 © CMP Media LLC


신고
http://www.kernelnewbies.org/faq/There are a couple of reasons:

(from Dave Miller) Empty statements give a warning from the compiler so this is why you see #define FOO do { } while(0).
(from Dave Miller) It gives you a basic block in which to declare local variables.
(from Ben Collins) It allows you to use more complex macros in conditional code. Imagine a macro of several lines of code like:

#define FOO(x)
        printf("arg is %sn", x);
        do_something_useful(x);

Now imagine using it like:

        if (blah == 2)
                FOO(blah);

This interprets to:

        if (blah == 2)
                printf("arg is %sn", blah);
                do_something_useful(blah);;

As you can see, the if then only encompasses the printf(), and the do_something_useful() call is unconditional (not within the scope of the if), like you wanted it. So, by using a block like do{...}while(0), you would get this:

        if (blah == 2)
                do {
                        printf("arg is %sn", blah);
                        do_something_useful(blah);
                } while (0);

Which is exactly what you want.
(from Per Persson) As both Miller and Collins point out, you want a block statement so you can have several lines of code and declare local variables. But then the natural thing would be to just use for example:

  #define exch(x,y) { int tmp; tmp=x; x=y; y=tmp; }

However that wouldn't work in some cases. The following code is meant to be an if-statement with two branches:

  if(x>y)
    exch(x,y);          // Branch 1
  else  
    do_something();     // Branch 2

But it would be interpreted as an if-statement with only one branch:

  if(x>y) {                     // Single-branch if-statement!!!
    int tmp;            // The one and only branch consists
    tmp = x;            // of the block.
    x = y;
    y = tmp;
  }
  ;                             // empty statement
  else                  // ERROR!!! "parse error before else"
    do_something();

The problem is the semi-colon (;) coming directly after the block.

The solution for this is to sandwich the block between do and while(0). Then we have a single statement with the capabilities of a block, but not considered as being a block statement by the compiler.

Our if-statement now becomes:

  if(x>y)
    do {
      int tmp;
      tmp = x;
      x = y;
      y = tmp;
    } while(0);
  else
    do_something();



신고

변수사용에 관하여...

Posted 2003.11.26 16:16
http://comeng.andong.ac.kr/%7Esoftware/report/edie/ch10.htm 10 변수 사용시 일반적으로 주의할 사항

10 변수 사용시 일반적으로 주의할 사항

 

범위(scope)

지속도(persistence)

결합시간(binding time)

데이터 구조(data structure) 제어구조(control structure) 관계

변수를 가지 목적으로만 사용하는 방법

전역변수(global variables)

 

 

10.1 범위(scope)

범위 변수의 유명도, 어떤 변수가 프로그램 전체에 얼마나 알려져 있는가를 측정하는 방법이다.

범위를 최소화 하라.

변수 참조부(reference) 변수를 가까운 곳에 놓아라.

 

 

10.2 지속도(persistence)

지속도 변수의 생존기간을 나타내는 용어다.

 

변수들이 가질 있는 지속도의 종류.

  • 특정 블록에서만 생존하는 변수.
  • 허락된 동안만 생존하는 변수.
  • 프로그램의 끝까지 생존하는변수.
  • 영원히 생존하는 변수.

 

 

10.3 결합시간 (binding time)

결합 시간이란, 변수에 값이 대입되는 시간을 뜻한다.

결합 시간은 가능한 늦추는 것이 이롭다. 일반적으로 결합 시간을 늦출수록, 코드의 융통성이 크다.

  • 코드 작성시에 변수에 값을 대입하는 코드    (바람직하지 않다)

TestID = 47;

  • 컴파일할 변수에 값을 대입하는 코드      (앞의 경우보다 훨씬 낫다)

#define MAX_ID 47

TestID = MAX_ID;

  • 프로그램을 실행할 변수에 값을 대입하는 코드     (문자값의 경우보다 판독성과 융통성이 좋다)

1. TestID = MaxID;

    2. TestID = ReadFileMaxID();

    3. TestID = GetMaxIDFromUser();

 

 

104. 데이터 구조와 제어 구조의 관계

잭슨의 가지 종류의 데이터 제어 구조의 관계

순차적 데이터는, 프로그램의 순차적 문장과 대응된다.

선택적 데이터는, 프로그램의 선택문과 대응된다.

반복형 데이터는, 프로그램의 for, repeat, while 루프 구조에 대응된다.

 

 

10.5 변수를 가지 목적으로만 사용하는 방법

변수를 가지 목적에만 사용하자.

숨겨진 의미가 있는 변수를 피하라.

선언된 변수가 모두 사용되는가 확인하라.

 

 

10.6 전역 변수 (global variable)

전역 변수를 사용하는 것이 지역변수를 사용하는 것보다 위험하다.

 

  • 전역 데이터와 관련된 일반적인 문제점들

전역 데이터의 값을 부주의하게 변경한 경우.  

(어떤 곳에서 전역 변수의 값을 바꾸어 놓고는, 다른 곳에서는 바뀌지않은 것으로 착각할 있다)

전역 변수에 의한별명문제.  

(같은 변수를 서로 다른 이름으로 호출하는 것을 말한다)

전역 데이터와 관련된 다중 진입 코드(re-entrant code) 문제점.  

(전역 데이터가 루틴들 사이에서만 공유되지 않고 같은 프로그램의 다른 복사본과도 공유될 가능성을 갖는다는 문제가 있다)

전역 데이터 때문에, 코드를 다시 사용할 없게 되는 문제점.  

(다시 사용하기를 원하는 모듈이 전역 데이터를 읽거나 쓴다면,   모듈을 프로그램에 단순하게 집어넣을 수가 없다. 경우, 프로그램 또는 이식할 모듈을 수정하여, 서로 호환성을 갖도록 보장해 주어야만 한다.  가장 바람직한 방법은,  이식할 모듈이 전역 데이터를 사용하지 않도록 수정하는 것이다)

프로그램의 모듈화와 능동적 유지 관리성(intellectual manageability) 방해하는 전역 데이터(전역 데이터는 프로그램의 모듈화를 방해한다. 만약 전역 데이터를 사용한다면 루틴에  집중할 없다.  지금 다루는 루틴과 함께 전역 데이터를 사용하는 다른 루틴들도 염두에 두어야 하기 때문이다)

  • 전역 변수를 사용하는 이유

전역값의 보존.

이름 붙은 상수(named constant) 대용품으로.  

(이름 붙은 상수(macro) 지원해주지 않는 언어에서 전역 변수를 대용품으로 있다)

매우 자주 사용되는 데이터의 합리화.  

(같은 변수를 자주 사용하는 경우)

통과 데이터의 제거.

 

  • 전역 데이터에 의한 위험을 줄이는 방법

일단 모든 변수들을 지역 변수로 선언하고, 필요한 것만 전역 변수로 전환하라.

전역 변수와 모듈 변수를 구분하라.

전역 변수의 용도를 쉽게 있도록, 변수의 명명 규칙(naming convention) 정하라.

여러분이 사용한 전역 변수들의 목록을 작성하라.

여러분이 포트란을 사용하고 있다면, 이름 붙은 COMMON(labeled COMMON) 사용하라.

전역 변수로 접근하는 것을 조절할 있도록, 로킹(locking) 사용하라.

거대 변수 모든 데이터를 집어넣고는 프로그램의 구석구석으로 보내는 방법으로, 전역 변수를 사용하지 않는 것처럼 꾸미지 말라.

 

  • 전역 변수를 대신하는 접근 루틴(access routine) 사용

전역 변수를 어떻게 사용하든지 간에, 접근 루틴을 사용하면 똑같은 작업을 잘할 있다.  접근 루틴은추상적 데이터형정보 숨기기 근간을 이루고 있다.

 

접근 루틴의 장점

  • 데이터를 집중적으로 제어할 있다.
  • 모든 변수 참조부들을 완벽하게 보호할 있다.
  • 정보 숨기기 이득을 저절로 얻게 된다.
  • 접근루틴은 추상적 데이터 형으로 쉽게 전환할 있다.

 

전역 변수를 대신한 접근 루틴의 사용법

 

데이터를 사용하는 모든 루틴들은, 반드시 접근 루틴을 통해서 데이터에 접근해야 한다.

모든 전역 변수를 단순히 같은 그릇에 담아서는 안된다.

접근 루틴에 추상화(abstraction)개념을 도입하라.

  • 복잡한 데이터를 직접 사용한 경우

    node = node.next

    node = node.next

    node = node.next

 

    Event = EventQueue[ QueueFront ]

    Event = EventQueue[ QueueBack ]

  • 복잡한 데이터를 접근 루틴을 통하여 사용한 경우

node = NearesrNeighbor( node )

node = NextEmployee( node )

node = NextRateLevel( node )

 

Event = HighestPriorityEvent()

Event = LowestPriorityEvent()

추상화 접근 루틴이 일반적인 데이터 구조보다 우리에게 훨씬 많은 정보를 준다.

데이터에 접근하는 방법의 추상화 수준을 일정하게 유지하라.

  • 복잡한 데이터를 일관성 없이 사용한 경우

    Event = EventQueue[ QueueFront ]

Event = EventQueue[ QueueBack ]

AddEvent(Event)

EventCount = EventCount ? 1

  • 복잡한 데이터를 일관성 있게 사용한 경우

Event = HighestPriorityEvent()

Event = LowestPriorityEvent()

AddEvent(Event)

RemoveEvent(Event)

접근 루틴은 전역 변수의 문제점을 해소하고 코드에 보다 많은 융통성을 부여한다.

신고
http://kldp.org/KoreanDoc/html/gcc_and_make/gcc_and_make-3.html--------------------------------------------------------------------------------
target:
        cd obj
        HOST_DIR=/home/e
        mv *.o $HOST_DIR
--------------------------------------------------------------------------------
하나의 목표에 대하여 여러 명령을 쓰면 예기치 않은 일이 벌어집니다. 기술적으로 말하자면 각 명령은 각자의 서브쉘에서 실행되므로 전혀 연관이 없습니다. -.- cd obj 도 하나의 쉘에서 HOST_DIR=/home/e도 하나의 쉘에서 나머지도 마찬가지입니다. 각기 다른 쉘에서 작업한 것처럼 되므로 cd obj 했다 하더라도 다음번 명령의 위치는 obj 디렉토리가 아니라 그대로 변함이 없이 현재 디렉토리입니다. 세번째 명령에서 HOST_DIR 변수를 찾으려 하지만 두번째 명령이 종료한 후 HOST_DIR 변수는 사라집니다.
--------------------------------------------------------------------------------
target:
        cd obj ;
        HOST_DIR=/hom/e ;
        mv *.o $$HOST_DIR
--------------------------------------------------------------------------------
이렇게 적어주셔야 합니다. 세미콜론으로 각 명령을 구분하지요. 처음 두 줄의 마지막에 쓰인 역슬래쉬() 문자는 한 줄에 쓸 것을 여러 줄로 나누어 쓴다는 것을 나타내고 있습니다.

주의! 세번째 줄에 $HOST_DIR이 아니라 $$HOST_DIR인 것을 명심하십시요. 예를 하나 들어보죠. ^^
--------------------------------------------------------------------------------
all:
         HELLO="안녕하세요?";
         echo $HELLO
--------------------------------------------------------------------------------
Makefile의 내용을 이렇게 간단하게 만듭니다.
$ make
HELLO="안녕하세요?";
echo ELLO
ELLO
<verb>

우리가 원하는 결과가 아니죠?

$HELLO를 $$HELLO로 바꾸어보십시요.

<verb>
$ make
HELLO="안녕하세요?";
echo $HELLO
안녕하세요?
--------------------------------------------------------------------------------
all:
         @HELLO="안녕하세요?"; echo $$HELLO
--------------------------------------------------------------------------------

명령의 맨 처음에 @ 문자를 붙여봅시다.

$ make
안녕하세요?


신고

'Research > Programming' 카테고리의 다른 글

Why do a lot of #defines in the kernel use do { ... } while(0)?  (0) 2004.12.18
변수사용에 관하여...  (0) 2003.11.26
make:하나의 목표에 대하여 여러 명령을 쓰면  (0) 2002.12.05
#, ##  (0) 2002.12.04
C Bit Fields  (0) 2002.12.04
__cdecl을 사용하는 이유 ?  (0) 2002.12.04

#, ##

Posted 2002.12.04 13:21
※ 프리프로세서문을 위한 연산자

프리프로세서문 내에서만 사용할 수 있는 연산자들이 있는데, 다음과 같이 3개가 있다.

┌────────────────────────────────────────────┐
│ #, ##, defined                                                                                                                │
└────────────────────────────────────────────┘

#과 ##은 주로 #define을 사용하여 매크로를 정의할 때 사용되는 연산자들로 #은 바로 뒤의 인

자를 스트링으로 바꾸어주는 역할을 한다. 예를 들어 어떤 정수 변수의 이름과 그 값을 출력시키

는 매크로를 작성하고자 할 때 다음과 같이 하면 된다.

┌────────────────────────────────────────────┐
│ #define PRI(x) printf(#x "= %dn",x)                                                                                │
└────────────────────────────────────────────┘

위 예에서 x앞에 '#' 연산자를 사용하고 있다. 위와 같이 정의한 후 다음과 같이 매크로를 사용

하게 되면

┌────────────────────────────────────────────┐
│ PRI(i);                                                                                                                          │
└────────────────────────────────────────────┘

이는 다음과 같이 바뀌게 된다.

┌────────────────────────────────────────────┐
│ printf("i" "= %dn",i);                                                                                                      │
└────────────────────────────────────────────┘

즉 #x에 의해 x가 스트링 x, 즉 "x"로 바뀌게 되어 "i"가 되며 두 개의 스트링을 연접해서 사용

하면 이는 하나의 스트링으로 간주되므로, 다음과 같은 결과가 된다.

┌────────────────────────────────────────────┐
│ printf("i = %dn",i);                                                                                                        
└────────────────────────────────────────────┘

'#' 연산자는 위와 같이 인자의 이름을 출력하거나 스트링에 포함시키고자 할 때 주로 사용하고

있다.

반면에 '##' 연산자는 x##y의 형태로 사용하는데, x와 y를 붙여 하나의 이름으로 만든다. 예를

들어 다음과 같이 사용할 수 있다.

┌────────────────────────────────────────────┐
│ #define CON(x,y) (x##y)                                                                                                │
└────────────────────────────────────────────┘

위와 같이 정의해 놓고 다음과 같이 사용하면

┌────────────────────────────────────────────┐
│ CON(i,1) = 100;                                                                                                             │
└────────────────────────────────────────────┘

이는 다음과 같이 바뀌게 된다.

┌────────────────────────────────────────────┐
│ i1 = 100;                                                                                                                        │
└────────────────────────────────────────────┘

즉 변수의 이름들을 둘로 나눌 수 있을 때, 또는 상황에 맞게 서로 다른 변수들을 사용하고자

할 때 ## 연산자를 사용하면 편한 경우가 많다.


신고

'Research > Programming' 카테고리의 다른 글

변수사용에 관하여...  (0) 2003.11.26
make:하나의 목표에 대하여 여러 명령을 쓰면  (0) 2002.12.05
#, ##  (0) 2002.12.04
C Bit Fields  (0) 2002.12.04
__cdecl을 사용하는 이유 ?  (0) 2002.12.04
register와 volatile 키워드의 역할  (0) 2002.12.04

C Bit Fields

Posted 2002.12.04 13:18
C Bit Fields
MSDN Home

C Bit Fields

In addition to declarators for members of a structure or union, a structure declarator can also be a specified number of bits, called a 밷it field.?Its length is set off from the declarator for the field name by a colon. A bit field is interpreted as an integral type.

Syntax

struct-declarator :

declarator
type-specifier declarator opt : constant-expression

The constant-expression specifies the width of the field in bits. The type-specifier for the declarator must be unsigned int, signed int, or int, and the constant-expression must be a nonnegative integer value. If the value is zero, the declaration has no declarator. Arrays of bit fields, pointers to bit fields, and functions returning bit fields are not allowed. The optional declarator names the bit field. Bit fields can only be declared as part of a structure. The address-of operator (&) cannot be applied to bit-field components.

Unnamed bit fields cannot be referenced, and their contents at run time are unpredictable. They can be used as 밺ummy?fields, for alignment purposes. An unnamed bit field whose width is specified as 0 guarantees that storage for the member following it in the struct-declaration-list begins on an int boundary.

Bit fields must also be long enough to contain the bit pattern. For example, these two statements are not legal:

short a:17;        /* Illegal! */
int long y:33;     /* Illegal! */

This example defines a two-dimensional array of structures named screen.

struct
{
    unsigned short icon : 8;
    unsigned short color : 4;
    unsigned short underline : 1;
    unsigned short blink : 1;
} screen[25][80];

The array contains 2,000 elements. Each element is an individual structure containing four bit-field members: icon, color, underline, and blink. The size of each structure is two bytes.

Bit fields have the same semantics as the integer type. This means a bit field is used in expressions in exactly the same way as a variable of the same base type would be used, regardless of how many bits are in the bit field.

Microsoft Specific ?gt;

Bit fields defined as int are treated as signed. A Microsoft extension to the ANSI C standard allows char and long types (both signed and unsigned) for bit fields. Unnamed bit fields with base type long, short, or char (signed or unsigned) force alignment to a boundary appropriate to the base type.

Bit fields are allocated within an integer from least-significant to most-significant bit. In the following code

struct mybitfields
{
    unsigned short a : 4;
    unsigned short b : 5;
    unsigned short c : 7;
} test;

void main( void );
{
    test.a = 2;
    test.b = 31;
    test.c = 0;
}

the bits would be arranged as follows:

00000001 11110010
cccccccb bbbbaaaa

Since the 8086 family of processors stores the low byte of integer values before the high byte, the integer 0x01F2 above would be stored in physical memory as 0xF2 followed by 0x01.

END Microsoft Specific

Contact Us | E-mail this Page | MSDN Flash Newsletter
?2001 Microsoft Corporation. All rights reserved. Terms of Use Privacy Statement Accessibility
신고

'Research > Programming' 카테고리의 다른 글

make:하나의 목표에 대하여 여러 명령을 쓰면  (0) 2002.12.05
#, ##  (0) 2002.12.04
C Bit Fields  (0) 2002.12.04
__cdecl을 사용하는 이유 ?  (0) 2002.12.04
register와 volatile 키워드의 역할  (0) 2002.12.04
volatile  (0) 2002.12.04

__cdecl을 사용하는 이유 ?

Posted 2002.12.04 13:17
대개의 언어들은 함수 또는 프로시져를 호출할 때 스택을 통해서 인자를 전달합니다. 이 때 인자를 전달하는 방식이 여러가지가 있을 수 있습니다. 예를 들어 첫번째 인자를 먼저 스택에 넣을 수도 있고, 아니면 마지막 인자를 먼저 스택에 넣을 수도 있겠죠. 또한, 스택에 넣은 인자를 누가 제거하느냐의 차이가 있을 수도 있습니다. (호출한 함수가 할 수도 있으며, 호출당한 함수가 할 수도 있습니다) C 방식은 인자 전달시 (순서가 어떻게 되는지는 잊어버렸는데) 호출한 함수에서 다시 인자를 꺼내서 제거하는 방식입니다. 어셈블리 코드로 보면

push argument1
push argument2 (1이 먼저인지 2가 먼저인지는 가물가물...)
call function
pop argument2
pop argument1 (물론 이보다 좀 효율적인 stack pointer를 직접 고치지만...)

보통은 이러한 __cdecl을 쓸 필요가 없는데, (C에서는 기본 전달 방식이니까) 문제는 C형식의 인자 전달방식을 사용하지 않는 라이브러리와 연결할 때 입니다. 가장 좋은 예가 M$사의 Windows의 API들입니다. Windows의 API들은 PASCAL 방식의 인자 전달 방식을 사용합니다. 역시 인자 전달 순서는 기억나지 않고, 호출당한 함수에서 stack의 인자를 제거합니다. 어셈블리어로 보면

procedure ()
....
pop argument2
pop argument1

push argument1
push argument2
call procedure

Windows의 API가 이렇게 구성된 이유는 여러가지가 있겠지만, 그 중 한가지는 x86 시스템에서 PASCAL 방식의 인자 전달방식이 더 효율적이기 때문입니다. ret 어셈블리 명령에서 직접 stack pointer를 조정할 수 있기 때문이죠. ret 8 하면 함수에서 돌아감과 동식에 stack pointer를 8만큼 조정하게 됩니다.

이렇게 한가지 이상의 방식으로 함수를 호출할 수 있으므로, 해당 함수가 어떠한 방식으로 사용되는지에 대해 정의할 필요가 있습니다. 이것이 __cdecl의 역할이 되겠습니다. Windows 함수들을 잘 살펴보시면 (include 디렉토리안의 헤더파일들) __cdecl 대신 PASCAL로 선언되어 있는 함수를 찾으실 수 있을 것입니다.

물론 UNIX의 경우 시스템 함수들이 C로 구현되어 있기 때문에 이러한 경우가 거의 없습니다. (파스칼로 만들어진 함수들과 연동하기 위해서는 필요하겠죠)
신고

'Research > Programming' 카테고리의 다른 글

#, ##  (0) 2002.12.04
C Bit Fields  (0) 2002.12.04
__cdecl을 사용하는 이유 ?  (0) 2002.12.04
register와 volatile 키워드의 역할  (0) 2002.12.04
volatile  (0) 2002.12.04
What Is Alignment  (0) 2002.12.04
register와 volatile 키워드는 해당 변수의 메모리 적재와 관련되어 있으며, 어떤 의미에서는 상반된 역할을 수행하도록 컴파일러에 지시합니다.

register 키워드
일반적인 변수선언의 형태처럼 아래와 같이 선언했다면,

int i;

해당 변수가 전역 변수로 선언된 경우에는 프로그램의 데이터 영역에, 지역 변수로 선언된 경우에는 스택 영역에 변수의 위치가 할당됩니다. 지역 변수가 스택 영역에 할당되는건, 해당 언어의 스펙과 구현에 따라 다를 수 있습니다. 대개의 경우 재귀호출을 허용하는 언어인 경우에는 스택 영역에, 그렇지 않은 언어의 경우에는 데이터 영역에 지역 변수를 할당하게 됩니다. 조금 말이 길었지만 결국 메모리 어딘가에 전역 변수든 지역변수든 할당된다는 것입니다. 예를 들어 아래와 같은 코드가 있다고 가정해 봅시다.

int i, sum, limit;
...
for (i=0; i < limit; i++) {
  sum = sum + i;
}
...

앞에서 설명한 바와 같이 위의 코드에서는 i, sum, limit 변수 모두 메모리 어딘가에 할당됩니다. 여기서 메모리(변수) 관련 연산들을 살펴보면 i를 0으로 초기화, i와 limit을 비교, i에 1을 증가, sum을 i만큼 증가시키는 연산이 필요하게 됩니다. 해당 변수값이 메모리에 할당되어 유지되므로 필요한 메모리 연산을 계산해보면, 루프를 한 번 수행하는 동안 sum 변수는 읽기 1번, 쓰기 1번, i 변수는 읽기 3번, 쓰기 1번, limit 변수는 읽기 1번이 필요합니다. 모두 합해서 7 * limit 횟수 만큼의 메모리 읽고 쓰기가 필요하게 되겠습니다.

메모리 연산은 CPU의 여러 연산 중 상대적으로 느리게 동작하는 연산 중에 하나입니다. 그나마, 80년대 초까지는 CPU의 클럭 속도가 낮은 상태에서는 큰 부담이 없었지만 지금은 그 차이가 무척 크지요. 이러하 CPU와 메모리 사이의 속도의 현격한 차이가 CPU 내의 1차 캐쉬 그리고 외부에 2차 캐쉬 또는 3차 캐쉬까지 제공되는 이유가 된 것입니다. 결론적으로 위에서 설명한 바와 같이 모든 경우에 메모리에 있는 변수를 읽고 쓴다면 CPU가 메모리 읽기 및 쓰기 연산이 수행되는 동안 상당한 수준 대기해야 함으로 CPU 본래의 성능에 비해 프로그램이 느리게 동작하게 됩니다.

CPU 내에는 그 갯수가 한정적이지만, 메모리처럼 사용할 수 있는 메모리 공간이 있습니다. 바로 레지스터입니다. 어셈블리어 책을 보면, 해당 같은 연산이 레지스터를 이용한 경우와 메모리를 이용한 경우의 두가지로 나오는 경우가 있습니다. CPU의 종류에 따라서 메모리 참조방식에 따라 여러 개 나오기도 하지요. 이러한 동일연산의 수행시간을 살펴보면, 레지스터를 참조하는 경우가 메모리를 참조하는 경우보다 빠른 것을 알 수 있습니다.

이 정도면 어느 정도 짐작하겠지만, register 키워드는 프로그램의 최적화를 위해서 해당 변수를 가능한한 (레지스터 갯수가 한정되어 있으니까요) 메모리 대신 레지스터에 넣어서 사용하라는 얘기입니다. 위의 예에서 아래와 같이 코딩을 하였다면,

register int i, limit, sum;
...
for (i=0; i < limit; i++) {
  sum = sum + i;
}

for 루프를 수행하기 전에 Ra, Rb, Rc라는 레지스터에 각각 i, limit, sum의 값을 먼저 옮기고, 루프를 수행합니다. 물론 루프 수행중에는 메모리 대신 해당 레지스터를 참조하겠지요. 루프 수행이 마치고 나면, 다시 메모리 변수 i, limit, sum의 값을 레지스터 Ra, Rb, Rc에서 읽어 저장하게 됩니다. 그럼 모두합해서 읽기 3번, 쓰기 3번 총 6번의 메모리 접근만 일어나게 되겠지요. 당연히 빨라집니다.

물론 이런 수고를 사람이 직접 해야 하느냐? 그건 아닙니다. 앤만한 경우에는 컴파일러가 알아서 해주게 됩니다. 괜히 있는 레지스터를 놀릴만컴 컴파일러가 둔하지는 않습니다. 컴파일러가 판단하기에 자주 그리고 많이 사용되겠다는 변수를 우선순위를 매긴 다음, 현재 남아 있는 레지스터 가운데 적절히 할당합니다. 레지스터 갯수가 별루 많지도 않고 특정 연산에는 특정 레지스터를 사용해야 하는 x86 CPU는 이러한 레지스터 할당이 힘들겠지만 그래도 최소 SI, DI 레지스터 일반 용도로 사용할 수 있습니다. 최신 x86 CPU에서는 레지스터 수가 좀 늘었는지 모르겠군요. 대부분의 RISC CPU는 수개에서 수십개까지의 일반 목적으로 활용할 수 있는 레지스터를 보유하고 있으니, 적절히 레지스터에 변수를 할당하게 되면 무척 빨라지겠죠. 이 때문에 RISC CPU에서는 컴파일러의 성능이 특히나 중요하게 여겨지게 됩니다. 물론 사용자가 register 키워드를 통해서 직접 제어한다면, 컴파일러에 의한 변수의 레지스터 할당에 비해 우선적으로 처리되겠습니다.

정말 똑똑한 컴파일러라면 위의 코드를 아래와 같이 바꾸겠지요.

int i, limit, sum;
...
sum = (1 + limit) * limit / 2;
i = limit;

volatile 키워드
volatile의 경우 어떤 의미에서는 앞에서 설명한 컴파일러의 최적화와 관계있습니다. 그 외에도 CPU 내, 외부의 캐쉬와 갈은 하드웨어적인 최적화와도 관계가 있습니다.

volatile 키워드가 가장 많이 사용되는 경우의 하나가 memory-mapped I/O인 경우입니다. 메모리의 특정 영역을 특정 장치와 연결하여 사용하는 방법입니다. 가장 흔한 예가 비디오 메모리가 되겠고, 그 이외에도 많은 장치들을 이러한 식으로 사용될 수 있습니다.

즉, 자신 엄밀하게 말한다면 컴파일러가 컴파일하고 있는 코드의 상황과는 관계 없이 바뀔 수 있는 메모리 변수가 있다면, 해당 변수에 대해 특별한 최적화를 하지 못하도록 컴파일러를 제약하는 키워드가 volatile입니다.

예를 들어, 0x0C000000 번지에 특별한 장치가 있다고 하겠습니다. 이 장치가 일종의 센서라고 하고 입력되는 센서값의 범위에 따라 다른 동작을 수행하게 프로그래밍을 아래와 같이 하였다고 하면,

int *p = 0x0C000000;
while (1) {
  if (*p == 0) {
    break;
  } else if  (*p < lower_limit) {
    action 1;
  } else if (*p < upper_limit) {
    action 2;
  }
  wait 10 mili-seconds
}

똑똑한 컴파일러는 위의 코드를 아래와 아래와 같이 바꿉니다.

int *p = 0x0C000000;
register int pvar = *p;
if (pvar == 0) {
} else if (pvar < lower_limit) {
    while (1) {
        action 1;
        wait 10 mili-seconds;
    }
  } else if (pvar < upper_limit) {
    while (1) {
        action 2;
        wait 10 mili-seconds;
    }
}

위와 같이 코드가 변형된다면, 프로그래머가 의도한 바와는 다른 결과가 나타나게 됩니다. 이러한 최적화를 억제하는 목적으로 volatile을 사용합니다. 컴파일러는 volatile 키워드가 선언된 변수에 대해서는 무조건 메모리에 접근하여 됩니다. 이러한 memory-mapped I/O 이외에도, 쓰레드 등으로 프로그램을 만들어서 공유변수를 한쪽에서 읽고, 한쪽에서 쓰는 경우도 해당될 수 있으며, 시스템 시간과 같이 특정 위치의 변수값이 자신과는 독립적으로 계속 변하는 경우에도 사용할 수 있습니다.

또는 CPU 내 외부의 캐쉬에 의해서도 이러한 최적화 효과가 나타날 수 있습니다. 캐쉬내에 해당 메모리 번지 값이 저장되어 읽을 때마다 같은 값이 읽히고, 또한 적을 때도 실제 메모리에 저장되지 않고 캐쉬에 임시 보관될 수 있습니다. 다른 경우는 잘 모르겠지만 MIPS의 R 시리즈 CPU에서는 (아마 2000 인가 3000 시리즈로 기억됩니다) 메모리를 두가지 방법으로 접근할 수 있습니다. 메모리 주소의 최상위 비트가 0이면 캐쉬를 거친 일반적인 접근을, 최상위 비트가 1이면 똑같은 주소의 메모리를 캐쉬를 거치지 않고 직접 접근할 수 있습니다. 당연히 이런 컴퓨터의 컴파일러에서 volatile로 선언하면 0C000000이 아니라 8C000000의 메모리를 접근하게 됩니다.
신고

'Research > Programming' 카테고리의 다른 글

#, ##  (0) 2002.12.04
C Bit Fields  (0) 2002.12.04
__cdecl을 사용하는 이유 ?  (0) 2002.12.04
register와 volatile 키워드의 역할  (0) 2002.12.04
volatile  (0) 2002.12.04
What Is Alignment  (0) 2002.12.04

volatile

Posted 2002.12.04 13:15
Volatile
By Nigel Jones
Embedded Systems Programming
(07/02/01, 12:20:57 PM EDT)


The use of volatile is poorly understood by many programmers. This is not surprising, as most C texts dismiss it in a sentence or two.

Have you experienced any of the following in your C/C++ embedded code?


Code that works fine-until you turn optimization on
Code that works fine-as long as interrupts are disabled
Flaky hardware drivers
Tasks that work fine in isolation-yet crash when another task is enabled
If you answered yes to any of the above, it's likely that you didn't use the C keyword volatile. You aren't alone. The use of volatile is poorly understood by many programmers. This is not surprising, as most C texts dismiss it in a sentence or two. volatile is a qualifier that is applied to a variable when it is declared. It tells the compiler that the value of the variable may change at any time-without any action being taken by the code the compiler finds nearby. The implications of this are quite serious. However, before we examine them, let's take a look at the syntax.

Syntax

To declare a variable volatile, include the keyword volatile before or after the data type in the variable definition. For instance both of these declarations will declare foo to be a volatile integer:

volatile int foo;
int volatile foo;

Now, it turns out that pointers to volatile variables are very common. Both of these declarations declare foo to be a pointer to a volatile integer:

volatile int * foo;
int volatile * foo;

Volatile pointers to non-volatile variables are very rare (I think I've used them once), but I'd better go ahead and give you the syntax:

int * volatile foo;

And just for completeness, if you really must have a volatile pointer to a volatile variable, then:

int volatile * volatile foo;

Incidentally, for a great explanation of why you have a choice of where to place volatile and why you should place it after the data type (for example, int volatile * foo), consult Dan Sak's column, 밫op-Level cv-Qualifiers in Function Parameters?(February 2000, p. 63).

Finally, if you apply volatile to a struct or union, the entire contents of the struct/union are volatile. If you don't want this behavior, you can apply the volatile qualifier to the individual members of the struct/union.

Use

A variable should be declared volatile whenever its value could change unexpectedly. In practice, only three types of variables could change:


Memory-mapped peripheral registers
Global variables modified by an interrupt service routine
Global variables within a multi-threaded application
Peripheral registers

Embedded systems contain real hardware, usually with sophisticated peripherals. These peripherals contain registers whose values may change asynchronously to the program flow. As a very simple example, consider an 8-bit status register at address 0x1234. It is required that you poll the status register until it becomes non-zero. The na?e and incorrect implementation is as follows:

UINT1 * ptr = (UINT1 *) 0x1234;

// Wait for register to become non-zero.
while (*ptr == 0);
// Do something else.

This will almost certainly fail as soon as you turn the optimizer on, since the compiler will generate assembly language that looks something like this:



mov ptr, #0x1234
mov a, @ptr
loop bz loop

The rationale of the optimizer is quite simple: having already read the variable's value into the accumulator (on the second line), there is no need to reread it, since the value will always be the same. Thus, in the third line, we end up with an infinite loop. To force the compiler to do what we want, we modify the declaration to:



UINT1 volatile * ptr =
(UINT1 volatile *) 0x1234;

The assembly language now looks like this:



mov ptr, #0x1234
loop mov a, @ptr
bz loop

The desired behavior is achieved.

Subtler problems tend to arise with registers that have special properties. For instance, a lot of peripherals contain registers that are cleared simply by reading them. Extra (or fewer) reads than you are intending can cause quite unexpected results in these cases.

Interrupt service routines

Interrupt service routines often set variables that are tested in main line code. For example, a serial port interrupt may test each received character to see if it is an ETX character (presumably signifying the end of a message). If the character is an ETX, the ISR might set a global flag. An incorrect implementation of this might be:



int etx_rcvd = FALSE;

void main()
{
...
while (!ext_rcvd)
{
// Wait
}
...
}

interrupt void rx_isr(void)
{
...
if (ETX == rx_char)
{
etx_rcvd = TRUE;
}
...
}

With optimization turned off, this code might work. However, any half decent optimizer will 밷reak?the code. The problem is that the compiler has no idea that etx_rcvd can be changed within an ISR. As far as the compiler is concerned, the expression !ext_rcvd is always true, and, therefore, you can never exit the while loop. Consequently, all the code after the while loop may simply be removed by the optimizer. If you are lucky, your compiler will warn you about this. If you are unlucky (or you haven't yet learned to take compiler warnings seriously), your code will fail miserably. Naturally, the blame will be placed on a 뱇ousy optimizer.?

The solution is to declare the variable etx_rcvd to be volatile. Then all of your problems (well, some of them anyway) will disappear.

Multi-threaded applications

Despite the presence of queues, pipes, and other scheduler-aware communications mechanisms in real-time operating systems, it is still fairly common for two tasks to exchange information via a shared memory location (that is, a global). When you add a pre-emptive scheduler to your code, your compiler still has no idea what a context switch is or when one might occur. Thus, another task modifying a shared global is conceptually identical to the problem of interrupt service routines discussed previously. So all shared global variables should be declared volatile. For example:


int cntr;

void task1(void)
{
cntr = 0;
while (cntr == 0)
{
sleep(1);
}
...
}

void task2(void)
{
...
cntr++;
sleep(10);
...
}

This code will likely fail once the compiler's optimizer is enabled. Declaring cntr to be volatile is the proper way to solve the problem.

Final thoughts

Some compilers allow you to implicitly declare all variables as volatile. Resist this temptation, since it is essentially a substitute for thought. It also leads to potentially less efficient code.

Also, resist the temptation to blame the optimizer or turn it off. Modern optimizers are so good that I cannot remember the last time I came across an optimization bug. In contrast, I come across failures to use volatile with depressing frequency. If you are given a piece of flaky code to 밼ix,?perform a grep for volatile. If grep comes up empty, the examples given here are probably good places to start looking for problems.

Nigel Jones is a consultant living in Maryland. When not underwater, he can be found slaving away on a diverse range of embedded projects. He can be reached at NAJones@compuserve.com.

Return to July 2001 Table of Contents

from : http://knol.google.com/k/vivek-bhadra/volatile-variables/3c84lj4klzp0d/16#

Context : C Language
Volatile is a qualifier that is applied to a variable when it is declared. It tells the compiler that the value of the variable may change at any time-without any action being taken by the code the compiler finds nearby. The implications of this are quite serious. However, before we examine them, let's take a look at the syntax.

A variable should be declared volatile whenever its value could change unexpectedly. In practice, only three types of variables could change:

  • Memory-mapped peripheral registers

  • Global variables modified by an interrupt service routine

  • Global variables within a multi-threaded application

Problems caused by not using the Volatile are

  • Code that works fine-until you turn optimization on

  • Code that works fine-as long as interrupts are disabled

  • Flaky hardware drivers

  • Tasks that work fine in isolation-yet crash when another task is enabled

static int foo;

void bar(void)
{
    foo = 0;
    while (foo != 255)
      continue;
}

In this example, the code sets the value stored in foo to 0. It then starts to poll that value repeatedly until it changes to 255.

An optimizing compiler will notice that no other code can possibly change the value stored in foo, and therefore assume that it will remain equal to 0 at all times. The compiler will then replace the function body with an infinite loop, similar to this:

 

void bar_optimized(void)
{
    foo = 0;
    while (TRUE)
        continue;
}

 

volatile is a qualifier that is applied to a variable when it is declared. It tells the compiler that the value of the variable may change at any time-without any action being taken by the code the compiler finds nearby. The implications of this are quite serious. However, before we examine them, let's take a look at the syntax.

 

Syntax

 

To declare a variable volatile, include the keyword volatile before or after the data type in the variable definition. For instance both of these declarations will declare foo to be a volatile integer:

 

volatile int foo;
int volatile foo;

 

Now, it turns out that pointers to volatile variables are very common. Both of these declarations declare foo to be a pointer to a volatile integer:

 

volatile int * foo;
int volatile * foo;

 

Volatile pointers to non-volatile variables are very rare (I think I've used them once), but I'd better go ahead and give you the syntax:

 

int * volatile foo;

 

And just for completeness, if you really must have a volatile pointer to a volatile variable, then:

 

int volatile * volatile foo;

 

Incidentally, for a great explanation of why you have a choice of where to place volatile and why you should place it after the data type (for example, int volatile * foo), consult Dan Sak's column, "Top-Level cv-Qualifiers in Function Parameters" (February 2000, p. 63).

 

Finally, if you apply volatile to a struct or union, the entire contents of the struct/union are volatile. If you don't want this behavior, you can apply the volatile qualifier to the individual members of the struct/union.

 

Use

Peripheral registers

 

Embedded systems contain real hardware, usually with sophisticated peripherals. These peripherals contain registers whose values may change asynchronously to the program flow. As a very simple example, consider an 8-bit status register at address 0x1234. It is required that you poll the status register until it becomes non-zero. The nave and incorrect implementation is as follows:

 

UINT1 * ptr = (UINT1 *) 0x1234;

 

// Wait for register to become non-zero.
while (*ptr == 0);
// Do something else.

 

This will almost certainly fail as soon as you turn the optimizer on, since the compiler will generate assembly language that looks something like this:

 

    mov    ptr, #0x1234     

    mov    a, @ptr

    loop     bz    loop

 

The rationale of the optimizer is quite simple: having already read the variable's value into the accumulator (on the second line), there is no need to reread it, since the value will always be the same. Thus, in the third line, we end up with an infinite loop. To force the compiler to do what we want, we modify the declaration to:

 

UINT1 volatile * ptr =
    (UINT1 volatile *) 0x1234;

 

The assembly language now looks like this:

 

    mov     ptr, #0x1234
    loop    mov  a, @ptr        
    bz    loop

 

The desired behavior is achieved.

 

Subtler problems tend to arise with registers that have special properties. For instance, a lot of peripherals contain registers that are cleared simply by reading them. Extra (or fewer) reads than you are intending can cause quite unexpected results in these cases.

 

Interrupt service routines

 

Interrupt service routines often set variables that are tested in main line code. For example, a serial port interrupt may test each received character to see if it is an ETX character (presumably signifying the end of a message). If the character is an ETX, the ISR might set a global flag. An incorrect implementation of this might be:

 

int etx_rcvd = FALSE;

 

void main()
{
    ...
    while (!ext_rcvd)
    {
        // Wait
    }
    ...
}

 

interrupt void rx_isr(void)
{
    ...
    if (ETX == rx_char)
    {
        etx_rcvd = TRUE;
    }
    ...
}

 

With optimization turned off, this code might work. However, any half decent optimizer will "break" the code. The problem is that the compiler has no idea that etx_rcvd can be changed within an ISR. As far as the compiler is concerned, the expression !ext_rcvd is always true, and, therefore, you can never exit the while loop. Consequently, all the code after the while loop may simply be removed by the optimizer. If you are lucky, your compiler will warn you about this. If you are unlucky (or you haven't yet learned to take compiler warnings seriously), your code will fail miserably. Naturally, the blame will be placed on a "lousy optimizer."

 

The solution is to declare the variable etx_rcvd to be volatile. Then all of your problems (well, some of them anyway) will disappear.

 

Multi-threaded applications

 

Despite the presence of queues, pipes, and other scheduler-aware communications mechanisms in real-time operating systems, it is still fairly common for two tasks to exchange information via a shared memory location (that is, a global). When you add a pre-emptive scheduler to your code, your compiler still has no idea what a context switch is or when one might occur. Thus, another task modifying a shared global is conceptually identical to the problem of interrupt service routines discussed previously. So all shared global variables should be declared volatile. For example:

 

int cntr;

 

void task1(void)
{
    cntr = 0;
    while (cntr == 0)
    {
        sleep(1);
    }
    ...
}

void task2(void)
{
    ...
    cntr++;
    sleep(10);
    ...
}

This code will likely fail once the compiler's optimizer is enabled. Declaring cntr to be volatile is the proper way to solve the problem.

Some compilers allow you to implicitly declare all variables as volatile. Resist this temptation, since it is essentially a substitute for thought. It also leads to potentially less efficient code.

Also, resist the temptation to blame the optimizer or turn it off. Modern optimizers are so good that I cannot remember the last time I came across an optimization bug. In contrast, I come across failures to use volatile with depressing frequency.

If you are given a piece of flaky code to "fix," perform a grep for volatile. If grep comes up empty, the examples given here are probably good places to start looking for problems. 

 

 

 

Can you have constant volatile variable?

You can have a constant pointer to a volatile variable but not a constant volatile variable.
 
 

Effect of volatile Keyword

    To describe in short what volatile keyword means, consider two blocks of a program, where second block is the same as first but with volatile keyword. Gray text between lines of C code means i386/AMD64 assembler compiled from this code.

{
    BOOL flag = TRUE;

    while( flag );
repeat:
    jmp repeat
}

{
    volatile BOOL flag = TRUE;
    mov        dword ptr [flag], 1

    while( flag );
repeat:
    mov        eax, dword ptr [flag]
    test       eax, eax
    jne        repeat
}

    In first block variable 'flag' could be cached by compiler into a CPU register, because it has not volatile qualifier. Because no one will change value at a register, program will hang in an infinite loop (yes, all code below this block is unreachable code, and compiler such as Microsoft Visual C++ knows about it). Also this loop was optimized in equivalent program with the same infinite loop, but without involving variable initialization and fetching. For those who don't know i386 assembler, 'jmp label' means the same as 'goto label' in C code.
    Second block have volatile qualifier and have more complex assembler output (initializing 'flag' with 'mov' instruction, in a loop fetching this flag into CPU register 'eax' with a 'mov' instruction, comparing fetched value with zero with 'test' instruction, and returning to the beginning of the loop if 'flag' was not equal to zero. 'jne' means 'goto if not equal'). This is all because volatile keyword prohibits compiler to cache variable value into CPU register, and it is fetched in all loop iterations. Such code is not always is an infinite loop, because another thread in the same program potentially could change value of variable 'flag' and first thread will exit the loop.
    It is important to understand that volatile keyword is just a directive for compiler and it works only at a compile-time. For example, the fact of using interlocked operation differs from just a compiler option, since special assembler commands are produced. Thus, interlocked instructions are most like to hardware directives, and they work at a run-time.


신고

'Research > Programming' 카테고리의 다른 글

#, ##  (0) 2002.12.04
C Bit Fields  (0) 2002.12.04
__cdecl을 사용하는 이유 ?  (0) 2002.12.04
register와 volatile 키워드의 역할  (0) 2002.12.04
volatile  (0) 2002.12.04
What Is Alignment  (0) 2002.12.04

What Is Alignment

Posted 2002.12.04 13:14

What Is Alignment?

Simple data types declared as variables are assigned addresses that are multiples of the size (in bytes) of the data type. Thus, variables of type long will be assigned addresses that are multiples of 4 (the bottom 2 bits of the address are 0).

Structures are padded so that each element of the structure will end up at a naturally aligned address. For example:

struct x_
{
   char a;     // 1 byte
   int b;      // 4 bytes
   short c;    // 2 bytes
   char d;     // 1 byte
} foo;

will actually be laid in memory so that it looks like this:

struct x_
{
   char a;            // 1 byte
   char _pad0[3];     // padding to put 'b' on 4-byte boundary
   int b;            // 4 bytes
   short c;          // 2 bytes
   char d;           // 1 byte
   char _pad1[1];    // padding to make sizeof(x_) multiple of 4
}

The result in both cases is that sizeof(struct x_) will return 12. Note that char _pad0[3] is included so that the int b member will be on a 4-byte boundary. The reason char _pad1[1] is included at the end is so that arrays of this structure will all have the same alignment; i.e.:

struct _x bar[3];

so that this array can be laid out as follows:

adr
offset   element
------   -------
0x0000   char a;         // bar[0]
0x0001   char pad0[3];
0x0004   int b;
0x0008   short c;
0x000a   char d;
0x000b   char _pad1[1];

0x000c   char a;         // bar[1]
0x000d   char _pad0[3];
0x0010   int b;
0x0014   short c;
0x0016   char d;
0x0017   char _pad1[1];

0x0018   char a;         // bar[2]
0x0019   char _pad0[3];
0x001c   int b;
0x0020   short c;
0x0022   char d;
0x0023   char _pad1[1];

This is important so that each member of each array element can be accessed naturally.

신고

'Research > Programming' 카테고리의 다른 글

#, ##  (0) 2002.12.04
C Bit Fields  (0) 2002.12.04
__cdecl을 사용하는 이유 ?  (0) 2002.12.04
register와 volatile 키워드의 역할  (0) 2002.12.04
volatile  (0) 2002.12.04
What Is Alignment  (0) 2002.12.04