15PB實地培訓學習筆記系列 DAY 2 -- 你懂Hello World嗎?反正我不懂

前言

沒有前言!!!

本篇的中心思想是:簡單Hello world,大學問。

 

說明一下更新的問題!!! 因爲我已經有了一些基礎了,所以記筆記就比較隨意了。可能突然想起來某些點子,某些有意思的東西我就會嘗試記下來。

 

最簡單的程序

學習任何語言貌似都會以一個“Hello World”進行開始,當然C語言程序也毫不例外。它幾乎堪稱是所有程序猿的入門第一個程序。來看看C語言的Hello world:

#include <stdio.h>
int main(int argc, char* argv[])
{
    printf("Hello world!");
    return 0;
}

是不是一股熟悉的感覺就來了?這就是新手程序員都會寫的Hello world。 來,再來看一段:

#include <stdio.h>
int main()
{
    printf("Hello world!");
    return 0;
}

以及,這種:

#include <stdio.h>
void main()
{
    printf("Hello world!");
}

以及這種也能行:

#include <stdio.h>
int main(int argc, char* argv[], int n)
{
    printf("Hello world!");
    return 0;
}

還有這種:

#include <stdio.h>
int main(int argc)
{
    printf("Hello world!");
    return 0;
}

看到這裏可能就比較疑惑了,C語言裏面定義,main函數的定義只有類似於這兩種

int main( int argc, char *argv[ ] )

int main( )

 

上面的其他的定義的形式都沒有見過的, 那麼帶有疑問的同學你去敲一下這個代碼發現也是可以被正常編譯和正常執行的,那麼爲什麼呢?

 

C語言main函數的調用和定義--來自於main函數的大學問

可能已經有不少的同學已經有了一些疑惑了。main函數爲什麼定義了這麼多的形式程序還能正常編譯和運行?會不會是編譯器在編譯的時候針對這些不同的函數進行了適配? 難道編譯器根據main函數的參數來決定如何調用的?

當然了,編譯器非常強大,但是還沒有這麼智能。 編譯器並沒有每次都根據不同的定義來決定函數的調用和參數傳遞。可能已經有同學想到了爲什麼了。

 

函數的調用約定

Windows下函數的調用和參數傳遞當然必然是按照某一種規定進行的,一定是參數按照某種方式進行傳遞的,函數的調用方式也是按照某一種規定來進行調用的。

  1. __cdecl,這是C語言默認的函數調用約定,它規定C函數調用時,參數從右至左依次壓棧,然後由調用者來清除壓入的參數;因爲由調用者來清除參數的數據,所以被調用函數甚至完全可以不用管調用者到底傳入了多少個參數,到底有沒有傳遞;這就以至於__cdecl調用約定的函數函數原型可以不一致。參考鏈接__cdecl
  2. __stdcall, 這是Windows的默認函數調用約定,可能你在看函數介紹什麼的,都會注意到很多的函數聲明前面都會加上一個WINAPI、__stdcall、CALLBACK等等之類的修飾符;並且調用者壓入的參數由被調用者進行清除,這就要求了傳入參數的個數必須和被調用函數的參數個數保持完全一致;
  3. __fastcall 這是64位程序函數調用會默認使用的調用約定(而且64位程序貌似也只使用__fastcall,當然32位程序也可以使用),這種調用約定採用寄存器進行參數傳遞,然後由被調用者進行參數清理。參考鏈接_fastcall
  4. __thiscall,這是C++類成員函數的調用約定,其中this指針通過ecx(rcx)進行傳遞,其餘參數以及堆棧清理和__cdecl一致。
  5. __vectorcall, 該調用約定完全採用寄存器進行據說傳遞。
  6. __clrcall, 顧名思義這是託管代碼的調用約定,不適用於native code. 詳見MSDN

 

看了上面的調用約定介紹,爲啥main能有這麼多種形式也很明瞭了。就是因爲main函數採用的就是__cdecl調用約定。可以試一下在main函數的前面加上一個其他的調用約定聲明。 這個時候你會發現在編譯的時候會出錯,會提示找不到main函數的定義,原因就是因爲C編譯器在鏈接的時候要求一個調用約定必須爲__cdecl的main函數。

 

誰纔是真正的入口點兒函數?

C語言裏面規定,main函數是所有代碼的入口點函數,所有代碼的執行都從main開始(tls和全局變量除外0),我們所有寫的代碼都是從main函數開頭。那麼是誰來調用的main函數呢?

 

我這裏來一段程序:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("Hello world");
    system("pause");
    return 0;
}

執行之後,停下來,這個時候看一下調用堆棧。

這裏注意一下,右下角的Call Stack窗口裏面,最上面我看到了我們的main函數,然後下面還有很多的函數調用,說好的main函數是入口點呢。怎麼最後居然還是一個其他的模塊裏面去了。

其實,main函數只是C語言規定的源程序入口函數,不是整個程序的入口;

真正程序的入口是     Helloworld.exe!mainCRTStartup() 這個函數; 那麼這個函數從什麼地方來的呢。這個函數是編譯器在編譯的時候就已經自動在程序裏面加上了這個函數,他纔是程序真正的入口點。

但是我們觀察在 mainCRTStartup函數的下面還有幾個函數,那麼這些函數又是怎麼回事呢。這裏就說到線程的創建了,操作系統在創建線程的時候,會將線程的入口地址指向爲RtlUserThreadStart(不同操作系統版本等原因這個名稱會不一致)然後由這個函數開始執行進入真正的用戶代碼入口。然後這些函數裏面到底做了什麼事情呢。以後有機會再說吧。

 

結語

簡單的“Hello world”程序裏面當然不簡單,這裏也只是解釋了其中的一點點。裏面還包含了很多的初始化操作。包括內存,異常處理程序等初始化。 如果是解釋型型語言的Hello world其原理還更復雜一些。比如,Java代碼運行需要藉助於Java 虛擬機,C#的代碼需要藉助.Net Framework框架,這類程序在運行時需要運行一個解釋器,然後由解釋器去不停的解釋和運行生成的中間代碼。自然流程就更復雜了。

 

預告, 下一章講Windows 的異常處理機制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章