2008年2月26日 星期二

Beginning Linux Programming 學習筆記(3) - Shell Programming (Shell Syntax)

前言

在 Linux 中,shell 的功能是相當強大的,許多系統管理的功能都可以由 shell 來完成,跟其他程式語言相同,shell 也有其特定語法格式,分成以下幾個部分來說明。


Variables

變數定義的部分,必須注意到引號(單雙引號)的使用與影響,以下用範例來說明:
#!/bin/sh

myvar="Hi there"

echo $myvar
echo "$myvar"
echo '$myvar' #使用單引號就不會印出變數內容
echo $myvar #escape

echo Enter some text
read myvar

echo '$myvar' now equals $myvar
exit 0

# ========== 執行結果 ==========
# Hi there
# Hi there
# $myvar
# $myvar
# Enter some text
# Hello World! <== 這個部分是使用者輸入的
# $myvar now equals Hello World!

接著說明 Environment Variables,這個部分大多數是使用者個人的設定,以下列出幾個常用的環境變數:
Environment Variables
描述
範例
$HOME
使用者家目錄
/home/godleon
$PATH
PATH 設定(由冒號所隔開)
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
$PS1
命令提示字元
${debian_chroot:+($debian_chroot)}\u@\h:\w\$
$PS2
命令提示字元
>
$IFS
用來隔開輸入參數的字元,通常為 space、tab 或 newline

$0
shell 的名稱
-bash
$#
傳入參數的數量
2
$$
shell script 所屬的 process ID
5465


繼續是 Parameter Variables,其用途很明顯,當然是跟傳入的參數有關:
Parameter Variables
描述
範例
$1, $2, ......
傳入的參數
para1, para2, .....
$*
所有傳入的參數,以 $IFS 隔開
para1 para2 para3 (假設 $IFS 為一個 space)
$@
所有傳入的參數,不過並非由 $IFS 隔開,而是用一個 space 作間隔
para1 para2 para3

以下用一個範例說明 environment/parameter variables 的使用:
#/bin/bash

salutation="Hello"
echo $salutation

echo "The program $0 is now running" # $0 (shell script 名稱)
echo "The second parameter was $2" # $2 (第二個參數)
echo "The first parameter was $1" # $1 (第一個參數)
echo "The parameter list was $*" # 參數列表
echo "The user's home directory is $HOME" # 家目錄
echo "Please enter a new greeting"
read salutation # 讀取使用者的輸入

echo $salutation
echo "The script is now complete"
exit 0

# ========== 執行結果 ==========
# Hello
# The program try_var.sh is now running
# The second parameter was two
# The first parameter was one
# The parameter list was one two
# The user home directory is /home/godleon
# Please enter a new greeting
# Hello Bash Shell
# Hello Bash Shell
# The script is now complete

Conditions

在 shell script 中可以用 test 指令來測試命令執行後所產生的 exit code,因此在每一個 shell script 的最後一行,建議都加上 exit code 來回應程式執行的結果。

在 shell 中,目前有三種 condition type,分別列表如下:
String Comparison
結果
str1 = str2
True,若 str1 等於 str2
str1 != str2
True,若 str1 不等於 str2
-n str
True,若 str 不為 null
-z str
True,若 string 為 null 或空白字串
Arithmetic Comparison
結果
exp1 -eq exp2
True,若兩個結果相同
exp1 -ne exp2
True,若兩個結果不相同
exp1 -gt exp2
True,若 exp1 大於 exp2
exp1 -ge exp2
True,若 exp1 大於等於 exp2
exp1 -lt exp2
True,若 exp1 小於 exp2
exp1 -le exp2
True,若 exp 小於等於 exp2
! exp
True,若 exp 為 false
File Conditional
結果
-d file
True,若檔案為目錄
-e file
True,若檔案存在
-f file
True,若檔案為一般檔案
-g file
True,若檔案有設定 set-gid
-r file
True,若檔案可讀取
-s file
True,或檔案大小不為 0
-u file
True,若檔案有設定 set-uid
-w file
True,若檔案可寫入
-x file
True,若檔案可執行

以下用一個簡單範例來說明使用方式:
#!/bin/bash

if [ -f /bin/bash ]
then
echo "file /bin/bash exists"
fi

if [ -d /bin/bash ]
then
echo "/bin/bash is a directory"
else
echo "/bin/bash is not a directory"
fi

# ========== 執行結果 ==========
# file /bin/bash exists
# /bin/bash is not a directory


Control Structures

shell 提供了許多不同的控制結構,以下分別一一介紹:

IF
#!/bin/bash

echo "Is it morning? Please answer yes or no"
read timeofday

if [ $timeofday = "yes" ]
then
echo "Good morning"
elif [ $timeofday = "no" ]; then # 請注意此處,必須加上分號以及另外一個 then 喔!
echo "Good afternoon"
else
echo "sorry, $timeofday not recongnized. Enter yes or no"
exit 1
fi

exit 0

FOR


這個部分需要注意一下,變數必須以 space 為間隔!
#!/bin/bash

# 會將 $(ls *sh) 的結果,以 space 為間隔印出
for file in $(ls *.sh);
do
echo $file
done
exit 0

WHILE

#!/bin/bash

echo "Enter password"
read trythis

while [ $trythis != "secret" ];
do
echo "sorry, try again"
read trythis
done
exit 0

UNTIL

#!/bin/bash

# 無使用者登入
until who | grep $1 > /dev/null
do
sleep 60
done

# 偵測到使用者登入
echo "$1 has just logged in"
exit 0

CASE

#!/bin/bash

echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
yes)
echo "Good morning";; # 每個 statement 結束都必須用兩個分號
no)
echo "Good afternoon";;
y)
echo "Good morning";;
n)
echo "Good afternoon";;
*)
echo "sorry, answer not recongnized";;
esac
exit 0
#!/bin/bash

echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
yes | y | Yes | YES) # 可用 regular expression 一次篩選多個條件
echo "Good morning" # 可執行多個指令
echo "Ip bright and early this morning"
;; # 每個 statement 結束都必須用兩個分號
[nN]*)
echo "Good afternoon";;
*)
echo "sorry, answer not recongnized"
echo "Please answer yes or no"
exit 1
;;
esac
exit 0

LISTs

其實講白了,這個就是 AND & OR
#!/bin/bash

touch file_one
rm -f file_two

# AND 的用法
if [ -f file_one ] && echo "hello" && [ -f file_two ] && echo " there"
then
echo "in if(and)"
else
# 由於 file_two 已經刪除,因此執行到此處
echo "in else(and)"
fi

# OR 的用法
if [ -f file_one ] || echo "hello" || echo " there"
then
# 執行到此處,因為 -f file_one 為 true
echo "in if(or)"
else
echo "in else(or)"
fi
exit 0


Functions

function 的呼叫,除了必須注意是否有回傳值之外,還要注意 global/local variables 的問題! 以下用範例來說明:

沒有回傳值的使用範例:
#!/bin/bash

sample_text="global variable" # 全域變數

foo() {
local sample_text="local variable" # 定義為區域變數
echo "Function foo is executing"
echo $sample_text # 區域變數
}

echo "script starting"
echo $sample_text # 全域變數

foo

echo "script ended"
echo $sample_text # 全域變數
exit 0
有回傳值的使用範例:
#!/bin/bash

# function 的宣告中不會帶參數,必須在呼叫時帶入
yes_or_no() {
echo "Is your name $* ?" # 印出帶入的參數
while true
do
echo -n "Enter yes or no: "
read x
case $x in
y | yes)
return 0;; # 0 表示回傳 true
n | no)
return 1;; # 1 表示回傳 false
*)
echo "Answer yes or no"
esac
done
}

echo "Original parameter are $*"

if yes_or_no "$1" # 帶入第一個參數
then
echo "Hi $1, nice name"
else
echo "never mind"
fi
exit 0


Commands

break

用法其實跟其他程式語言是相同的,作為跳出回圈之用,而 continue 的用法也相同。
#!/bin/bash

rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4

for file in fred*
do
if [ -d $file ]
then
break; # 效果同 C 語言中的 break
fi
done

echo "first directory starting fred was $file"

rm -rf fred*
exit 0

.
(dot)

dot,透過 dot 會讓在 script 中的指令不會進入 sub shell 執行
# ========== dot.sh ==========
#!/bin/bash

PS2="###"
exit 0


# ========== 執行結果 ==========
# 原本的 $PS2
# shell> echo $PS2
# >

# 一般執行方式
# shell> sh dot.sh
# shell> echo $PS2 # 進入 sub shell 執行,因此還是沒有修改到 parent shell 中的設定
# >

# 以 dot 方式來執行
# shell> . dot.sh
# shell> echo $PS2
# ###

eval


將組合而成的字串變成有意義的變數
#!/bin/bash

foo=10
x=foo
y='$'$x
echo $y # 輸出 => $foo

# 使用 eval 來處理(類似 * 將 pointer 所代表的值提取出來的功能)
eval y='$'$x
echo $y # 輸出 => 10

export

在 sub shell 中定義環境變數之用
# ==================== export1.sh ====================
#!/bin/bash

foo="The first meta-syntactic variable"
export bar="The second meta-syntactic variable" # export 後即為環境變數

sh export2.sh


# ==================== export2.sh ====================
#!/bin/bash

echo $foo
echo $bar # 只有此行會顯示出內容,因為經過 export

expr


執行類似其他程式語言的基本數學運算處理

一般來說,在 shell programming 中是不能做數學運算的,因為 shell 把所有的值都視為字串(string),不過透過 expr 就可以解決這個問題,以下列出 expr 可以進行的簡單數學運算:
敘述
作用
expr1 | expr2
若 expr 不為 0,則回傳 expr1;反之回傳 expr2
expr1 & expr2或兩者任何一個為 0,則回傳 0;反之回傳 expr1
expr1 = expr2判斷是否相同
expr1 > expr2判斷 expr1 是否大於 expr2
expr1 >= expr2判斷 expr 是否大於等於 expr2
expr1 < expr2派斷 expr1 是否小於 expr2
expr1 <= expr2判斷 expr1 是否小於等於 expr2
expr1 != expr2判斷 expr1 是否不等於 expr2
expr1 + expr2回傳兩者相加的結果
expr1 - expr2回傳兩者相減的結果
expr1 * expr2回傳兩者相乘的結果
expr1 / expr2回傳兩者相除的結果
expr1 % expr2回傳餘數

接著用範例說明:
# x++
x = `expr $x + 1`
x = $(expr $x + 1)

printf


非常類似 C 語言中的 printf,但不支援浮點數的表示
shell> printf "%sn" Hello
Hello
shell> printf "%s %dt%s" "Hi there" 15 people
Hi there 15 people

set


在程式中設定參數
#!/bin/bash

echo the date is $(date)

# 設定參數(以 space 為區隔)
set $(date)

# 取第二個參數
echo The month is $2

exit 0


# ========== 執行結果 ==========
# shell> sh set.sh
# the date is 二 226 15:10:00 CST 2008
# The month is 2

shift


shift 參數之用,常用來瀏覽所有參數
#!/bin/bash

while [ "$1" != "" ]
do
echo $1
# $2->$1 $3->$2 $4->$3
shift
done

exit 0


# ========== 執行結果 ==========
# shell> sh shift.sh 1 2 3
# 1
# 2
# 3

trap


用來指定接收到特殊訊號時的處理動作

訊號(signal)的種類大致可分以下幾種:
訊號
描述
HUP (1)
Hang up (通常在 terminal 離線或使用者登出時會送出此類訊號)
INT (2)
Interrupt (按下 Ctrl + C)
QUIT (3)
Quit (按下 Ctrl + \)
ABRT (6)
Abort (通常程式執行發生嚴重錯誤時會送出此類訊號)
ALRM (14)
Alarm (通常用來處理逾時之用)
TERM (15)
Terminate (通常當機器要關機時,由系統送出)
#!/bin/bash

#若接收到 interrupt 的訊號,以下的命令就會執行
trap 'rm -f /tmp/my_tmp_file_$$' INT

echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$

echo "press interrupt (Ctrl+C) to interrupt ...."
while [ -f /tmp/my_tmp_file_$$ ]
do
echo File exist
sleep 1
done
echo The file no longer exists

#即使接收到 interrupt 的訊號,檔案也不會被刪除
trap INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$

#因此此處會進入無限回圈
echo "press interrupt (Ctrl+C) to interrupt ...."
while [ -f /tmp/my_tmp_file_$$ ]
do
echo File exist
sleep 1
done

echo we never get here
exit 0


# ========== 執行結果 ==========
# shell> sh trap.sh
# creating file /tmp/my_tmp_file_10581
# press interrupt (Ctrl+C) to interrupt ....
# File exist
# File exist
# File exist <=== 此處按下 Ctrl + C
# The file no longer exists
# creating file /tmp/my_tmp_file_10581
# press interrupt (Ctrl+C) to interrupt ....
# File exist
# File exist
# File exist <=== 此處按下 Ctrl + C

unset


刪除自己設定的環境變數
#!/bin/bash

foo="Hello World"
echo $foo

# unset 後,foo 變數就不存在了
unset foo
echo $foo


# ========== 執行結果 ==========
# shell> sh unset.sh
# Hello World
# (此處為空白)

Beginning Linux Programming 學習筆記(2) - Shell Programming (Pipes & Redirection)

Redirect

在說明 redirect 之前,必須知道 redirect 可以處理的,僅有 file descriptor 0(standard input), 1(standard output), 2(standard error) 三種,接著用例子說明使用方式:
# 將 ls -al 所產生的訊息輸出至檔案 lsoutput.txt 中(檔案內容會被覆蓋)
shell> ls -al > lsoutput.txt

# 將 ls -al 所產生的訊息附加至檔案 lsoutput.txt 中
shell> ls -al >> lsoutput.txt

# 使用可能會產生錯誤訊息的指令(例如 kill)
# 將 standard output 輸出至檔案 killout.txt 中
# 將 standard error 輸出至檔案 killerr.txt 中

shell> kill -HUP 1234 >killout.txt 2>killerr.txt

# 將 standard output & error 輸出到同一個檔案中
shell> kill -1 1234 >killouterr.txt 2>&1

# 不保留任何 standard output & error 相關訊息
shell> kill -1 1234 >/dev/null 2>&1

# 將 mydata.txt 檔案中的內容透過 more 指令呈現 (standard input)
shell> more < mydata.txt


Pipes

pipe 是將前一個 process 處理的結果交給下一個 process 繼續進行處理,藉而達到類似連結多個 process 的效果。

在 Linux 中,連接 process 的數量並沒有限制,並且還能跟 redirect 來搭配同時使用,以下用幾個範例來說明:
# (1) 取得 ps 相關資訊
# (2) 排序後將結果存入檔案 pssort.txt 中

shell> ps | sort > pssort.txt

# (1) 取得 ps 相關資訊
# (2) 排序
# (3) 使用 more 來展示

shell> ps | sort | more

# (1) 取得 ps 相關資訊
# (2) 排序
# (3) 過濾掉重複的 process name
# (4) 清除名稱為 sh 的 process
# (5) 使用 more 來展示

shell> ps -xo comm | sort | uniq | grep -v sh | more

# (1) 使用 cat 讀取 mydata.txt 的內容
# (2) 將內容進行排序
# (3) 過濾掉重複的資料,並將結果存回 mydata.txt

shell> cat mydata.txt | sort | uniq > mydata.txt

2008年2月23日 星期六

PC Assembly Language 學習筆記(8) - Arrays

陣列的定義

陣列是一個連續的記憶體空間,每個元素的大小、型態皆相同,在 assembly 中定義陣列的方式很簡單,以下用個範例來說明:
segment .data
;宣告長度為 10 的陣列,每個元素大小為 double word,並分別初始化元素的值為 1, 2, 3, ....., 10
a1 dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

;宣告長度為 10 的陣列,每個元素大小為 word,初始化所有元素的值為 0
a2 dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

;與上一個宣告相同,但使用 TIMES 關鍵字表示重複指令的次數
a3 times 10 dw 0

;宣告長度為 200 的陣列,每個元素大小為 word,每一個元素的值為 1
a4 times 200 dw 1

segment .bss
;宣告長度為 10 的陣列,每個元素大小為 double word,未初始化
a5 resd 10

;宣告長度為 200 的陣列,每個元素大小為 word,未初始化
a6 resw 200

而若要在在 stack 將陣列定義為 local 變數呢? 假設需要的空間有 1 個 char、2 個 double word、50 個 word 元素的陣列,一共需要 1 * 1 + 2 * 4 + 50 * 2 = 109 個 bytes 的空間,從之前介紹過的程式來看,有兩種方法可以達成,一個是透過 [ESP - 109] 的方式來保留記憶體空間,另一個則是用 ENTER 指令來保留空間,以下為保留記憶體空間後的 stack 圖示:



陣列的存取

在 assembly 中存取陣列的元素值,並非像 C 或其他高階語言一樣,使用中括號([])加索引的方式,而是必須要清楚指定 memory address(以 byte 為單位),以下用幾個範例來說明:
;定義兩個陣列,每個元素的大小分別為 byte 與 word
byteArray db 5, 4, 3, 2, 1
wordArray dw 5, 4, 3, 2, 1

;存取 byte 陣列
mov al, [byteArray] ;AL = byteArray[0]
mov al, [byteArray + 1] ;AL = byteArray[1]
mov [byteArray + 3], al ;byteArray[3] = AL

;存取 word 陣列
mov ax, [wordArray] ;AX = wordArray[0]
mov ax, [wordArray + 2] ;AX = wordArray[1] 注意! 這邊 + 2 bytes 即是 + 1 word
mov [wordArray + 6], ax ;wordArray[3] = AX
mov ax, [wordArray + 1] ;這是錯誤的用法,因為元素大小為 word(2 bytes)
以下介紹如何將陣列的值進行加總:
    mov     ebx, byteArray      ;將 byteArray 的初始位址存到 EBX 中
mov dx, 0 ;總和存於 DX 中
mov ah, 0 ;為了確保 AX = AL,因此要清空 AH
mov ecx, 0 ;loop 的次數以 ECX 中的值為主,每次遞減
lp:
mov al, [ebx] ;AL = *EBX
add dx, ax ;add 指令兩端 operand 的大小必須相同
inc ebx ;移至陣列的下一個元素
loop lp

接著用範例說明陣列的操作方式:
// ==================== array1c.c ====================
#include <stdio.h>

int asm_main(void);
void dump_line(void);

int main() {
int ret_status;
ret_status = asm_main();
return ret_status;
} //end main

void dump_line() {
int ch;
while((ch = getchar()) != EOF && ch != 'n')
;
} //end dump_line


; ==================== array1.asm ====================
%define ARRAY_SIZE 100
%define NEW_LINE 10

segment .data
FirstMsg db "First 10 elements of array", 0
Prompt db "Enter index of element to display: ", 0
SecondMsg db "Element %d is %d", NEW_LINE, 0
ThirdMsg db "Elements 20 through 29 of array", 0
InputFormat db "%d", 0

segment .bss
array resd ARRAY_SIZE

segment .text
extern puts, printf, scanf, dump_line
global asm_main
asm_main:
enter 4, 0 ;預留空間給 local 變數用,長度為 double word [EBP - 4]
push ebx ;保留 EBX 的值,先存入 stack 中
push esi ;保留 ESI 的值,先存入 stack 中

;初始化陣列值為 100, 99, 98, 97, .....
mov ecx, ARRAY_SIZE
mov ebx, array
init_loop:
mov [ebx], ecx
add ebx, 4
loop init_loop

push dword FirstMsg ;印出 FirstMsg
call puts
pop ecx

push dword 10
push dword array
call print_array ;印出陣列中的前十個元素
add esp, 8 ;使用 C function 後必須回復原本 ESP 指向的位址(使用 2 個 stack 空間)

Prompt_loop:
push dword Prompt
call printf
pop ecx ;將 Prompt 的 memory address pop 到 ECX 中(沒意義,只是將其從 stack 中移除)

lea eax, [ebp - 4] ;EAX = local 變數的位址
push eax
push dword InputFormat
call scanf
add esp, 8 ;使用 C function 後必須回復原本 ESP 指向的位址(使用 2 個 stack 空間)
cmp eax, 1 ;EAX = scanf 的 return value
je InputOK

call dump_line
jmp Prompt_loop

InputOK:
mov esi, [ebp - 4]
push dword [array + 4 * esi]
push esi
push dword SecondMsg
call printf
add esp, 12 ;使用 C function 後必須回復原本 ESP 指向的位址(使用 3 個 stack 空間)

push dword ThirdMsg
call puts
pop ecx ;將 ThirdMsg 的 memory address pop 到 ECX 中(沒意義,只是將其從 stack 中移除)

push dword 10
push dword array + 20 * 4
call print_array
add esp, 8 ;呼叫 function 後必須回復原本 ESP 指向的位址(使用 2 個 stack 空間)

pop esi ;還原 ESI 的值
pop ebx ;還原 EBX 的值
mov eax, 0 ;回到 C 程式中
leave
ret

; routine : _print_array
; C 程式
; void print_array(const int *a, int n);
; 參數:
; (1) a - 指向陣列的 pointer [EBP + 8]
; (2) n - 要印出的元素數量 [EBP + 12]
segment .data
OutputFormat db "%-5d %5d", NEW_LINE, 0

segment .text
global print_array
print_array:
enter 0, 0
push esi ;保留 ESI 的值,先存入 stack 中
push ebx ;保留 EBX 的值,先存入 stack 中

xor esi, esi ;ESI = 0
mov ecx, [EBP + 12] ;迴圈次數
mov ebx, [ebp + 8] ;EBX = 陣列位址
print_loop:
push ecx ;printf 可能會改變 ECX 中的值,因此先存入 stack 中作保留

;呼叫 C 語言中的 printf,使用的 stack 中的資料作為參數
;分別為(1)OutputFormat (2)ESI (3)陣列值
push dword [ebx + 4 * esi]
push esi
push dword OutputFormat
call printf
add esp, 12 ;使用 C function 後必須回復原本 ESP 指向的位址(使用 3 個 stack 空間)

inc esi ;index++
pop ecx ;還原 ECX
loop print_loop

pop ebx ;還原 EBX 的值
pop esi ;還原 ESI 的值
leave
ret


Array/String Instructions

在 80x86 的 CPU 中,設計了許多用來處理 array 的指令,這些指令稱為 string instruction,他們利用 index register(ESI、EDI) 做為索引之用,並搭配 FLAGS register 中的 direction flag(DF) 來判斷索引為遞增或遞減,其中有兩個指令可用來設定 direction flag:
  • CLD(clears the direction flag):將 DF 設定為 0,為遞增
  • STD(sets the direction flag):將 DF 設定為 1,為遞減

Reading & writing memory

首先先說明讀寫記憶體內容所使用的指令:

左邊的部分是 read memory,右邊則是 write memory;而從上面這個表中,有三個部分是需要注意的:
  1. 實際資料存放的位置
    LOASx 的部分,是存放在 data segment 中;STOSx 的部分則是存放在 extra segment 中
  2. 所使用到的 index register
    LOASx 使用的是 ESI(source index);STOSx 則是使用 EDI(destination index)
  3. 每次迴圈,index register 所遞增的值
    根據不同的資料長度,分別為 1(byte)、2(word)、4(double word)

此外,還可以注意到所使用的 segment 為 data segment(左邊) 與 extra segment(右邊),以下用一個簡單範例來介紹:
segment .data
ary1 dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

segment .bss
ary2 resd 10

segment .text
cld ;將 direction flag 設定為 0,讓 index register 的動作為遞增
mov esi, ary1 ;將 ary1 的 memory address 放到 ESI
mov edi, ary2 ;將 ary2 的 memory address 放到 EDI
mov ecx, 10 ;為了執行迴圈 10 次

lp:
;將每個在 ary1 中的元素複製到 ary2 中
lodsd ;load single double word
stosd ;store single double word
loop lp
而上述的效果,還可以用 MOVSx 指令來達成,而 MOVSx 指令的使用可以參考下表:

上述的 mov 所處理的資料大小是不同的,因此使用上就必須注意一下囉! 在上一個範例中,LODSD 與 STOSD 的結合,其實用 MOVSD 就可以達到相同效果囉!!!


REP 指令


講完 loop 的寫法後,一定要提到 REP 指令,其實這個指令就是 repeat 的意思,也是以 ECX 中的值作為重複次數的依據,而且可以用很簡單的語法就可以完成,以上面程式的最後三行為例(lp label 後的三行),可以改成以下這一行:
rep movsd ;MOVSD 的效果可以參考上表
以下用一段範例程式碼將陣列所有元素初始化為 0:
segment .bss
ary resd 10

segment .text
cld ;讓 index register 為遞增
mov edi, ary
mov ecx, 10
xor eax, eax ;下一個指令會用到 EAX
rep stosd ;[EDI] = EAX (使用 extra segment,EDI += 4)

Comparison 相關指令

接著這邊介紹的是用來進行比對的指令,這些指令用來比較 register 與 memory 的值,以下用一張圖來介紹有哪些指令可用:

以上這些指令,跟 CMP 相同,也是會修改 FLAGS register 中的值做為程式判斷的依據,以下用一段簡單的程式碼來說明使用方式:
segment .bss
ary resd 100

segment .text
cld ;index register 為遞增
mov edi, ary
mov ecx, 100
mov eax, 12 ;為了找尋陣列中有無元素值為 12

lp:
scasd ;比對 EAX 與 [ES:EDI] 的值
je found ;有尋找到,跳至 found label
loop lp
;=====================
;找不到的時候,所要執行的程式碼放這
;=====================

found:
sub edi, 4 ;確定目前 EDI 所儲存的為指向 12 元素的 pointer
;=====================
;找到以後,所要執行的程式碼放這
;=====================

REPx 指令

REPx 指令與 REP 是類似的,都是作為 repeat 指令之用,以下先用張圖來說明有哪些相關指令:

而 REPx 通常用來與 comparison 指令進行搭配,而若是有比對相同,程式並不會因此停止執行,index register 會持續遞增,而 ECX 則是會持續遞減,直到條件達到(Z flag=1/0 或 ECX=0)為止。

以下用一段比對 memory block 的程式來說明 REPx 的使用方式:
segment .text
cld
mov esi, block1 ;第一個 memory block 的 address
mov edi, block2 ;第二個 memory block 的 address
mov ecx, size
repe cmpsb ;持續的比對每一個 byte,直到 Z=1 或是 ECX=0
je equal

;=====================
;比對不同,所要執行的程式碼置於此處
;=====================

jmp onward ;必須跳離,不然會執行到 equal Label 去

equal:
;=====================
;比對相同,所要執行的程式碼置於此處
;=====================

onward:

範例說明

最後用一個較為完整的範例來說明這些指令的使用:

memory.asm
global  asm_copy, asm_find, asm_strlen, asm_strcpy

segment .text

; function: asm_copy
; 用途:複製 memory block
; C functin 定義 :
; void asm_copy(void *dest, const void *src, unsigned sz);
; 參數說明 :
; (1) dest - 指向目的地位址的 pointer
; (2) sec - 指向來源端位址的 pointer
; (3) sz - 要複製的 byte 數量
%define dest [ebp + 8]
%define src [ebp + 12]
%define sz [ebp + 16]

asm_copy:
enter 0, 0
push esi ;保留 ESI 與 EDI 的值(先存入 stack)
push edi

mov esi, src
mov edi, dest
mov ecx, sz

cld ;指定 index register 為遞增 (direction flag = 0)
rep movsb ;[ES:EDI] = [DS:ESI] (ESI += 4 、 EDI += 4)

pop edi ;還原 ESI 與 EDI 的值
pop esi
leave
ret


; function: asm_find
; 用途:在記憶體中尋找指定的 byte
; C functin 定義 :
; void *asm_find(const void *src, char target, unsigned sz);
; 參數說明 :
; (1) src - 指向來源端位址的 pointer
; (2) target - 所要尋找的 byte 值
; (3) sz - 所要尋找的 memory block 大小
; 回傳值說明 :
; 若找到指定的值,回傳指定該值的 pointer;若無,回傳 null
;
; 注意:尋找的是 byte,但因為使用到 stack,因此要把他當作 double word 來處理(使用 lower 8 bits)
%define src [ebp + 8]
%define target [ebp + 12]
%define sz [ebp + 16]

asm_find:
enter 0, 0
push edi

mov eax, target
mov edi, src
mov ecx, sz

cld
repne scasb ; scan memory block 的值,直到 ECX=0 或是 [ES:EDI]=AL

je found_it
mov eax, 0 ;未發現,回傳 null pointer
jmp short quit

found_it:
mov eax, edi ;由 C 呼叫 assembly function 時,回傳值必須存在 EAX 中(因為長度為 double word)
dec eax ;讓 pointer 指向正確的位址

quit:
pop edi
leave
ret


; function: asm_strlen
; 用途:回傳字串的長度
; C functin 定義 :
; unsigned asm_find(const char *);
; 參數說明 :
; (1) src - 指向字串的 pointer
; 回傳值說明 :
; 字串長度(會存入 EAX 中)
%define src [ebp + 8]

asm_strlen:
enter 0, 0
push edi

mov edi, src ;將 string pointer 放到 EDI 中
mov ecx, 0FFFFFFFFh ;設定 ECX 為最大
xor al, al ;AL=0

cld
repnz scasb ;尋找字串結尾(\0)

mov eax, 0FFFFFFFEh
sub eax, ecx ;length = 0FFFFFFFEh - ecx

pop edi
leave
ret


; function: asm_strcpy
; 用途:複製字串
; C functin 定義 :
; void *asm_strcpy(char *dest, const char *src);
; 參數說明 :
; (1) dest - 指向複製來源字串的 pointer
; (2) src - 指向複製目的地字串的 pointer
%define desc [ebp + 8]
%define src [ebp + 12]

asm_strcpy:
enter 0, 0
push esi
push edi

mov edi, dest
mov esi, src

cld
cpy_loop:
lodsb ;將字元讀取到 AL 中,並遞增 ESI 的值
stosb ;將字元從 AL 中讀取出來,並遞增 EDI 的值
or al, al ;設定 condition flags
jnz cpy_loop ;尚未遇到結束字元(\0),繼續執行

pop edi
pop esi
leave
ret
memex.c
#include <stdio.h>

#define STR_SIZE 30

void asm_copy(void *, const void *, unsigned);
void * asm_find(const void *, char target, unsigned);
unsigned asm_strlen(const char *);
void asm_strcpy(char *, const char *);

int main(void) {
char st1[STR_SIZE] = "test string";
char st2[STR_SIZE];
char *st;
char ch;

asm_copy(st2, st1, STR_SIZE); //呼叫 assembly function
printf("%s\n", st2);

printf("Enter a char: ");
scanf("%c%*[^\n]", &ch);
st = asm_find(st2, ch, STR_SIZE); //呼叫 assembly function
if(st)
printf("Found it: %s\n", st);
else
printf("Not Found\n");

st1[0] = 0;
printf("Enter string: ");
scanf("%s", st1);
printf("len = %u\n", asm_strlen(st1)); //呼叫 assembly function

asm_strcpy(st2, st1); //呼叫 assembly function
printf("%s\n", st2);

return 0;
} //end main

編譯、執行指令
shell> nasm -f elf memory.asm ; gcc -o memex memex.c memory.o ; ./memex

2008年2月21日 星期四

Google Gears 架構簡介

離線資料庫的考量

在一般的 web application 中,資料的儲存都是在 server 端負責的,因此一般的流程都是從 server 取得資料後在 client browser 中顯示,若要讓 web application 可以離線(offline)運作,有幾個因素必須要考量到:
  1. 必須將 data layer 的部分獨立出來
  2. 有哪些功能是需要離線運作的
  3. server 與 client 資料同步的問題


Google Gears 離線架構說明

以下用圖來比較以下一般的 web 架構與 Google Gears 所提出的離線 web 架構:

一般架構:
離線架構:
看了上面的架構圖後,其實應該也不太難揣測 Google Gears 是怎麼運作的了。

將一般架構與離線架構相比較,可以發現在離線架構中扮演最重要的角色的即為「Data Layer」與「Data Switcher」,說明以下兩者所負責的工作。

Data Layer

Data Layer 所負責的工作其實很簡單,最主要的部分是將 application 的資料需求 pass 到 server,並接收 server 所回來的資料,再將資料交給 application 做處理。

通常提供了一組 API 供 application 來呼叫,可能有些複雜的資料處理、轉換工作都會在 Data Layer 中完成。

Data Switcher

Data Switch 所扮演的角色就相對很重要了,在整個架構中,Data Switch 為 Application 與 Data Layer 的中介橋樑,在 Data Switch 中會決定來自 Application 的資料需求是要 pass 給 server,或是在 local 端進行處理。

因此,在考量系統中的哪些部分必須加入離線(Offline)運作的功能,或是何時需要使用到離線運作的功能,就必須實作於 Data Switch 中,此部分則端看系統實際的使用需求了! 有些功能是必須要 on-line 才有意義的,例如:即時通訊;基本上,存取越頻繁的資料越適合移到 local data layer 來處理,這樣可以大大減少 remote connect 的機會,提升系統效能。


離線系統的模式

離線系統的模式,大致上可以分為兩種,一種是讓使用者可以決定何時讓系統連線,何時離線,稱為 Modal;另一種則是會自動偵測網路連線情況來決定處理方式,稱為 Modeless。

當然不同的模式有不同的優缺點,以 Modal 為例,由於 on-line 與 off-line 的情況是由使用者決定,因此程式開發上較為容易,不過若是使用者搞不清楚狀況,可能就會有沒有資料可用的情形發生(在進入 off-line狀況之前忘了先抓取一些資料來用),或是僅有舊資料可用的狀況(一直忘記更新 server 中的資料);而 Modeless 的優缺點大致上跟 Modal 是相反的,因此開發較為複雜。

而開發時要選取何種模式,則是要根據系統實際會運用的狀況以及使用者對電腦系統操作的熟稔程度來決定。


資料同步

開發 off-line 的系統,資料同步是最重要的問題,這個部分有兩種方式可以達成,其中一種是讓使用者決定何時進行讓 local storage 與 server 的資料同步,通常在 UI 上擺著按鈕用來進行此項操作;而另外一種則是系統會自動偵測連線狀況,在不影響正常使用的情況下,每次一部份一部份的將 local storage 與 server 上的資料進行同步

讓使用者自行選擇資料同步時間點的作法是很容易的,但是也必須考慮到使用者對於資訊系統的操作是否瞭解;而比較困難的是自動偵測連線狀況,必須有一個額外的機制來處理資料同步的工作,通常稱為 Sync Engine,以下用一張圖來說明整個架構:


透過上面這張圖,可以看到 Sync Engine 必須與 local database 及 server(Internet) 進行溝通,因此這個部分的開發勢必較為困難,不過由於所有工作使隱藏起來在背景執行,因此不會影響到系統的使用;至於 Sync Engine 如何實作,最主要還是根據系統需求來決定囉!!

2008年2月20日 星期三

Beginning Linux Programming 學習筆記(1) - Library 簡介

簡介

撰寫 C 語言時,除了必須用的 header file 之外,另外一個重要角色,就是 library 了! 而 library 又分為兩種:
  1. static library (副檔名為 .a)
  2. shared library (副檔名為 .so.N)


Static Library

所謂的 static library,其實沒很特別,只是一堆 object file 的集合而已,以下介紹 static library 的用法。

假設有兩個 function 分別位於不同的 file 中:
// ==================== fred.c ====================
#include <stdio.h>

void fred(int arg) {
printf("fred: we passed %d\n", arg);
} //end fred

// ==================== bill.c ====================
#include <stdio.h>

void bill(char *arg) {
printf("bill: we passed %s\n", arg);
} //end bill
另外設計一支程式,使用到上面所定義的 function:
// ==================== program.c ====================
#include <stdlib.h>

#include "lib.h"

int main(void) {
bill("Hello World!");
exit(0);
} //end main
接著是編譯的方式,有幾種方式可以進行編譯:
# (1) 直接將主程式與 function file 一同編譯
shell> gcc -o program program.c bill.c

# (1) 將定義兩個 function 的檔案編譯成 object file
# (2) 將主程式與 object file 一同進行編譯

shell> gcc -c fred.c bill.c ; gcc -o program program.c bill.o

# (1) 將定義兩個 function 的檔案編譯成 object file
# (2) 產生 static library (archive)
# (3) 將主程式與 static library 一同進行編譯

shell> gcc -c fred.c bill.c ; ar -crv libfoo.a fred.o bill.o ; gcc -o program program.c libfoo.a

# (1) 將定義兩個 function 的檔案編譯成 object file
# (2) 產生 static library (archive)
# (3) 將主程式與 static library 一同進行編譯(-L 指定 library directory、-l 指令所要用的 library name)

shell> gcc -c fred.c bill.c ; ar -crv libfoo.a fred.o bill.o ; gcc -o program program.c -L. -lfoo
其中第三種是比較標準的作法,不過其實 gcc 的功能很強,所以在編譯的時候可以省略一些指令卻可以達到相同效果。


Shared Library

談到 shared library 之前,必須先說明 static library 的缺點,就是當很多程式都使用相同的 static library 時,會在 memory 中產生多份相同的 copy,無形之中造成記憶體很多浪費,因此才會有 shared library 的出現。

shared library 存放的位置跟 static library 大致上是相同的,不過副檔名為 .so.N (N 為重新修改的次數)。

使用 shared library,即時同時被多個程式所使用,系統也只要在 memory 中存有一份資料即可,比起 static library 是節省記憶體許多。

而其運作的方式,在一開始時,shared library 是不會被載入 memory 中,而是當主程式執行時(此時主程式被載入 memory 中),將主程式所使參照使用到的 share library 在執行期間載入 memory 中執行。

接著,用來處理主程式與 shared library 參照工作的程式,稱為 ld.so(或是 ld-linux.so.N),而相關設定檔位於 /etc/ld.so.conf 中,若是設定有任何改變,則需要執行 ldconfig 指令重建參照的工作。

最後,若要查詢一個程式執行時所需求的 shared library,可使用以下指令:
shell> ldd program
linux-gate.so.1 => (0xffffe000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e89000) # 需求 libc.so 這個 shared library
/lib/ld-linux.so.2 (0xb7fdf000)