一文了解程序鏈接過程之動(dòng)態(tài)鏈接
作者 大話IT
通過靜態(tài)鏈接,可以生成一個(gè)可執(zhí)行文件,這個(gè)可執(zhí)行文件既可以是完全鏈接的也可以是部分鏈接的,對(duì)于部分鏈接的可執(zhí)行文件,有些符號(hào)引用需要等到可執(zhí)行文件加載時(shí)甚至是運(yùn)行時(shí)才會(huì)進(jìn)行符號(hào)解析和重定位。
動(dòng)態(tài)鏈接與靜態(tài)鏈接一樣包括符號(hào)解析和重定位兩個(gè)任務(wù),靜態(tài)鏈接和動(dòng)態(tài)鏈接的區(qū)別之一就是符號(hào)解析和重定位的時(shí)機(jī),動(dòng)態(tài)鏈接分為加載時(shí)動(dòng)態(tài)鏈接和運(yùn)行時(shí)動(dòng)態(tài)鏈接,本篇文章將拆分成3個(gè)部分闡述:
1.可執(zhí)行文件的結(jié)構(gòu)和加載過程。
2.加載時(shí)動(dòng)態(tài)鏈接。
3.運(yùn)行時(shí)動(dòng)態(tài)鏈接。
可執(zhí)行文件的結(jié)構(gòu)和加載過程
可執(zhí)行文件的結(jié)構(gòu)
可執(zhí)行文件的結(jié)構(gòu)與可重定位目標(biāo)文件的結(jié)構(gòu)類似,都是采用ELF文件格式,不同的是它們包括的節(jié)有差異,另外可執(zhí)行文件增加了程序頭部表(program header table),如下圖所示
可執(zhí)行文件結(jié)構(gòu)圖
如上圖所示,對(duì)于可執(zhí)行文件來說,ELF文件頭中的程序入口地址字段不再為空,入口地址指向了第一條指令的虛擬內(nèi)存地址,.init是一個(gè)函數(shù),這個(gè)函數(shù)在程序加載時(shí),做一些初始化的工作。
可執(zhí)行文件中只有一部分內(nèi)容能夠加載到內(nèi)存中,如上圖所示,.init,.text,.rodata這幾個(gè)節(jié),程序頭部表以及ELF文件頭會(huì)加載到內(nèi)存中代碼段中,代碼段的權(quán)限只讀,.data,.bss這兩個(gè)節(jié)加載到內(nèi)存的數(shù)據(jù)段,數(shù)據(jù)段的權(quán)限是讀和寫,其它節(jié)不能加載到內(nèi)存中,只起到輔助作用。
可執(zhí)行文件與可重定位目標(biāo)文件相比較多出了一個(gè)程序頭部表,下面闡述下程序頭部表的作用。
程序頭部表:
程序頭部表負(fù)責(zé)將可執(zhí)行文件中連續(xù)的內(nèi)容映射到連續(xù)的內(nèi)存段,通過【objdump -dx 可執(zhí)行文件】可以查看可執(zhí)行文件的結(jié)構(gòu),其中可以看到程序頭部表的描述信息,如下圖
程序頭部表
由上圖得知程序頭部表包括了9個(gè)表項(xiàng),我們重點(diǎn)關(guān)注2個(gè)LOAD表項(xiàng)。
第一個(gè)LOAD表項(xiàng):這個(gè)表項(xiàng)用于將可執(zhí)行文件的.init,.text,.rodata這幾個(gè)節(jié),程序頭部表以及ELF文件頭的內(nèi)容映射到只讀內(nèi)存段即代碼段,其中off=0x0000000000000000表示從可執(zhí)行文件偏移0處開始映射,filesz=0x00000000000008ec表示映射范圍的大小,vddr=paddr=0x0000000000400000表示映射到代碼段的開始地址為0x0000000000400000,memsz=0x00000000000008ec表示映射的代碼段大小,flags=r-x表示代碼段是可讀,可執(zhí)行的,align=2的21次方表示代碼段的開始地址必須是2的21次方的整數(shù)倍,通常來說off%align=vadd%align。
第二個(gè)LOAD表項(xiàng):這個(gè)表項(xiàng)用于將可執(zhí)行文件的.data,.bss這兩個(gè)節(jié)的內(nèi)容映射到讀寫內(nèi)存段即數(shù)據(jù)段,其中off=0x0000000000000e00表示從可執(zhí)行文件偏移0x0000000000000e00處開始映射,filesz=0x000000000000024c表示映射范圍的大小,vddr=paddr=0x0000000000600e00表示映射到數(shù)據(jù)段的開始地址為0x0000000000600e00,memsz=0x0000000000000258表示映射的數(shù)據(jù)段大小,flags=rw-表示數(shù)據(jù)段是可讀,可寫的,align=2的21次方表示數(shù)據(jù)段的開始地址必須是2的21次方的整數(shù)倍,通常來說off%align=vadd%align。
可執(zhí)行文件的結(jié)構(gòu)就介紹到這里了,下面介紹下可執(zhí)行文件的加載過程。
可執(zhí)行文件的加載
我們經(jīng)常通過./prog 命令加載一個(gè)可執(zhí)行文件,prog是可執(zhí)行文件的名稱,當(dāng)執(zhí)行這個(gè)命令時(shí),操作系統(tǒng)會(huì)調(diào)用內(nèi)核中的一個(gè)加載器(loader),通過加載器來運(yùn)行可執(zhí)行文件,另外在Linux中也可以采用execve函數(shù)來調(diào)用加載器。
加載器加載一個(gè)可執(zhí)行文件時(shí),會(huì)執(zhí)行以下步驟:
1.創(chuàng)建一個(gè)進(jìn)程上下文和進(jìn)程虛擬地址空間,如下圖為進(jìn)程虛擬地址空間
2.建立可執(zhí)行文件的內(nèi)容(按照頁大。┖吞摂M地址空間頁的映射關(guān)系(依據(jù)程序頭部表),依據(jù)程序頭部表可以得出,代碼段總是從0x0000000000400000開始的,數(shù)據(jù)段則是從0x0000000000600e00開始的,代碼段的大小為0x00000000000008ec,數(shù)據(jù)段的大小為0x0000000000000258。
3.將可執(zhí)行文件的文件頭信息加載到內(nèi)存。
以上就是加載一個(gè)可執(zhí)行文件的過程,可以看出加載的過程并沒有把可執(zhí)行文件中的代碼和數(shù)據(jù)加載到內(nèi)存,當(dāng)CPU第一次調(diào)度進(jìn)程時(shí),加載器才會(huì)根據(jù)文件頭中的程序入口地址,開始執(zhí)行第一條指令,當(dāng)執(zhí)行這條指令時(shí),發(fā)現(xiàn)指令不在內(nèi)存中,發(fā)生了缺頁中斷,才會(huì)將虛擬頁加載到內(nèi)存中。
程序的入口地址往往指向_start函數(shù),_start函數(shù)調(diào)用系統(tǒng)啟動(dòng)函數(shù)__libc_start_main,這個(gè)系統(tǒng)啟動(dòng)函數(shù)定義在libc.so中,它負(fù)責(zé)初始化執(zhí)行環(huán)境,調(diào)用用戶層提供的main函數(shù),同時(shí)負(fù)責(zé)處理main函數(shù)的返回值,并在合適的時(shí)機(jī)交由內(nèi)核處理。
加載時(shí)動(dòng)態(tài)鏈接
靜態(tài)鏈接是在執(zhí)行鏈接命令時(shí)開始鏈接過程,通常鏈接的是靜態(tài)庫,另外靜態(tài)庫可以實(shí)現(xiàn)按需鏈接即用到了到哪個(gè)函數(shù)就鏈接哪個(gè)函數(shù)到可執(zhí)行文件中。
不過靜態(tài)庫有以下幾個(gè)弊端:
1.靜態(tài)庫的內(nèi)容發(fā)生了變化,其它依賴靜態(tài)庫的程序需要重新編譯。
2.靜態(tài)鏈接時(shí),將靜態(tài)庫的函數(shù)和數(shù)據(jù)復(fù)制到了可執(zhí)行文件中,因此多個(gè)可執(zhí)行文件就會(huì)有多份函數(shù)和數(shù)據(jù),這個(gè)會(huì)耗費(fèi)大量的磁盤空間,如果將這些可執(zhí)行文件加載到內(nèi)存則會(huì)耗費(fèi)大量的內(nèi)存。
因此,共享庫就誕生了,它在內(nèi)存中獨(dú)一份,共享庫是個(gè)共享目標(biāo)文件,也是采用ELF文件格式,通常是以.so作為文件后綴,這里順別提一下微軟的共享庫是DLL文件。
當(dāng)可執(zhí)行文件加載時(shí),由動(dòng)態(tài)鏈接器加載共享庫到內(nèi)存中,然后和可執(zhí)行文件進(jìn)行鏈接,因此這個(gè)過程叫做加載時(shí)動(dòng)態(tài)鏈接。
可以通過【gcc -shared -fpic -o allvector.so addvec.c multvec.c】命令生成一個(gè)共享庫,對(duì)于-fpic表示生成位置無關(guān)的共享庫,一般來說生成共享庫必須有這個(gè)選項(xiàng),后續(xù)會(huì)單獨(dú)介紹。
生成了共享庫后,可以通過【gcc -o proglib mainlib.c ./allvector.so】命令生成一個(gè)可執(zhí)行文件,當(dāng)然這個(gè)命令并也需要執(zhí)行靜態(tài)鏈接,通過靜態(tài)鏈接將一些需要靜態(tài)鏈接的目標(biāo)文件進(jìn)行鏈接,鏈接后生成一個(gè)部分鏈接的可執(zhí)行文件,當(dāng)可執(zhí)行文件加載時(shí),通過動(dòng)態(tài)鏈接器鏈接allvector.so這個(gè)共享庫。
可執(zhí)行文件加載時(shí),有一個(gè).interp節(jié),這個(gè)節(jié)包含了動(dòng)態(tài)鏈接器的文件路徑,動(dòng)態(tài)鏈接器也是一個(gè)共享庫,操作系統(tǒng)負(fù)責(zé)加載和運(yùn)行這個(gè)動(dòng)態(tài)鏈接器,然后由這個(gè)動(dòng)態(tài)鏈接器來負(fù)責(zé)加載共享庫,動(dòng)態(tài)鏈接器接下來共享庫的鏈接過程。
以【gcc -o proglib mainlib.c ./allvector.so】這個(gè)命令生成的可執(zhí)行文件為例,加載可執(zhí)行文件時(shí),動(dòng)態(tài)鏈接的過程大致如下:
1.加載共享庫libc.so這個(gè)共享庫中的數(shù)據(jù)和代碼到內(nèi)存段,并進(jìn)行符號(hào)解析和重定位。
2.加載共享庫allvector.so這個(gè)共享庫中的數(shù)據(jù)和代碼到內(nèi)存段,并進(jìn)行符號(hào)解析和重定位。
3.對(duì)proclib可執(zhí)行文件中涉及到的未定義的符號(hào)引用進(jìn)行解析和重定位。
可執(zhí)行文件運(yùn)行時(shí)動(dòng)態(tài)鏈接
除了在可執(zhí)行文件加載時(shí)進(jìn)行動(dòng)態(tài)鏈接,也可以在應(yīng)用程序運(yùn)行過程中,加載和運(yùn)行一個(gè)共享庫,然后進(jìn)行動(dòng)態(tài)連接。
運(yùn)行時(shí)動(dòng)態(tài)連接有兩個(gè)常見場(chǎng)景:
1.分發(fā)軟件包,例如采用共享庫來作為一個(gè)軟件升級(jí)包,用戶下載這個(gè)這個(gè)共享庫后,替換了舊版本,應(yīng)用程序運(yùn)行時(shí),會(huì)自動(dòng)加載這個(gè)共享庫并進(jìn)行重新鏈接。
2.構(gòu)建高性能的Web服務(wù)器,很多Web服務(wù)器接受客戶端的請(qǐng)求生成動(dòng)態(tài)的頁面內(nèi)容,早期的做法是創(chuàng)建一個(gè)子進(jìn)程(fork),然后在這個(gè)子進(jìn)程來生成動(dòng)態(tài)的頁面內(nèi)容,這樣的方式性能不是很好,不利于擴(kuò)展,而高性能的Web服務(wù)器則是將每個(gè)動(dòng)態(tài)生成頁面內(nèi)容的函數(shù)封裝成一個(gè)共享庫,當(dāng)服務(wù)器接受到客戶端請(qǐng)求時(shí),會(huì)動(dòng)態(tài)鏈接到合適的函數(shù),然后調(diào)用它,這個(gè)函數(shù)只加載一次,便會(huì)緩存在內(nèi)存中,下一次請(qǐng)求同一個(gè)函數(shù)時(shí),就是直接獲取這個(gè)函數(shù)指針即可,另外,如果函數(shù)發(fā)生變化時(shí),不需要重啟服務(wù)器,只需要重新加載這個(gè)共享庫就可以了,另外Web服務(wù)器也可以動(dòng)態(tài)增加一個(gè)新的函數(shù),來滿足新的業(yè)務(wù)需求。
Linux提供了運(yùn)行時(shí)加載共享庫的接口,如下所示:
#include <dlfcn.h>
// 打開一個(gè)共享庫,返回一個(gè)共享庫句柄
void *dlopen(const char*filenames, int flag);
// 根據(jù)一個(gè)共享庫句柄,查找某個(gè)函數(shù)名的指針
void *dlsym(void *handle, char*symbol);
// 根據(jù)共享庫句柄,關(guān)閉一個(gè)共享庫,如果沒有其它程序
// 引用這個(gè)句柄,則卸載這個(gè)共享庫
int dlclose(void *handle);
// 用于檢查dlopen,dlsym,dlclose操作是不是成功,如果不成功
//則返回錯(cuò)誤信息,否則返回NULL
const char *dlerror(void);
Java中要調(diào)用C函數(shù),通常采用JNI,原理就是將C函數(shù)編譯成一個(gè)共享庫(*.so),當(dāng)一個(gè)Java程序運(yùn)行時(shí),Java解釋器調(diào)用dlopen接口打開這個(gè)共享庫,然后通過函數(shù)名調(diào)用dlsymf返回函數(shù)地址,然后調(diào)用這個(gè)函數(shù)。
位置無關(guān)代碼
共享庫的目的是多個(gè)程序共享一份庫代碼,那么多個(gè)程序是怎么共享一份庫代碼呢,一種方式是將一個(gè)共享庫存儲(chǔ)在內(nèi)存的固定位置,然后程序動(dòng)態(tài)鏈接時(shí),將符號(hào)引用重定位到這個(gè)位置就可以了,然后這種方式也有不少弊端:
1.地址空間的使用率不高,例如一個(gè)共享庫即使沒有被使用,它也占用哪個(gè)空間,因?yàn)檫@個(gè)空間已經(jīng)被分配出來了。
2.一個(gè)共享庫的版本發(fā)生了變化后,占用的空間可能會(huì)擴(kuò)大,這樣很有可能需要選擇一個(gè)新的內(nèi)存段來存儲(chǔ)這個(gè)共享庫。
3.新的共享庫需要新的內(nèi)存段,隨著時(shí)間的推移,會(huì)有大量大小不一的共享庫,這就會(huì)造成很多不能使用的空閑內(nèi)存。
4.每個(gè)系統(tǒng),庫在內(nèi)存中的分配方式不同,這樣就增加了管理難度。
為了解決上述的弊端,現(xiàn)代系統(tǒng)編譯共享庫代碼,這個(gè)共享庫代碼是可以加載到內(nèi)存的任意位置的,所有用到這個(gè)共享庫代碼的程序,通過【gcc -shared -fpic -o allvector.so addvec.c multvec.c】的方式生成位置無關(guān)的代碼,其中-fpic就表示生成位置無關(guān)的代碼。
位置無關(guān)代碼的實(shí)現(xiàn)原理是基于以下一個(gè)事實(shí):
可執(zhí)行文件的代碼段中指令引用的數(shù)據(jù)變量的地址和函數(shù)的地址是相對(duì)固定的即位置無關(guān)的,如下圖:
代碼段和數(shù)據(jù)段的距離是固定的X
如上圖所示,代碼段的【指令n-1】引用到了數(shù)據(jù)段的【變量n】,當(dāng)執(zhí)行【指令n-1】時(shí),(%rip)表示下一條指令即【指令n】的地址,【指令n】的地址與變量n的地址位置差是X,這個(gè)X是固定的,與內(nèi)存無關(guān)的,因此執(zhí)行【指令n-1】時(shí),通過X[%rip]即指令n的地址+X總是能夠獲取正確的變量n的地址。
位置無關(guān)的代碼基于上面所述的事實(shí),可以分為兩類:全局變量引用的位置無關(guān)和函數(shù)引用的位置無關(guān),下面分別討論:
全局變量引用的位置無關(guān):
編譯器為了實(shí)現(xiàn)全局變量引用的位置無關(guān),引入了一個(gè)全局偏移量表(GOT),全局偏移量表為每個(gè)全局變量引用增加一個(gè)條目,每個(gè)條目占用8個(gè)字節(jié),每個(gè)條目都有一個(gè)重定位條目,當(dāng)可執(zhí)行文件加載時(shí),會(huì)根據(jù)重定位條目對(duì)全局變量進(jìn)行重定位,將條目的內(nèi)容設(shè)置為全局變量重定位后的地址,如下圖所示
全局變量GOT
上圖是基于如下的代碼
int addcnt = 0;
void addvec(int *x,int *y,int *z, int n)
{
int i;
addcnt++;
for(i = 0; i < n; i++){
z[i] = x[i] + y[i];
}
}
由上圖和上述代碼可以看出,addvec函數(shù)引用了全局變量addcnt,在addvec函數(shù)中執(zhí)行指令【mov 0x2008b9(%rip),%rax】時(shí),0x2008b9是當(dāng)前執(zhí)行指令的下條指令和GOT[3]的距離,這個(gè)距離是固定的,與內(nèi)存無關(guān)的,【mov 0x2008b9(%rip),%rax】指令通過訪問GOT[3]來間接獲取addcnt的全局變量的地址,當(dāng)addcnt全局變量的內(nèi)存地址發(fā)生變化時(shí),只需要將GOT[3]的內(nèi)容進(jìn)行調(diào)整,addvec的所有指令不需要進(jìn)行調(diào)整,這就實(shí)現(xiàn)了全局變量引用的位置無關(guān)。
函數(shù)引用的位置無關(guān):
編譯器為了實(shí)現(xiàn)函數(shù)引用的位置無關(guān),采用全局偏移量表GOT和過程鏈接表PLT共同來完成的,如下圖所示:
PLT
上圖基于如下代碼
int addcnt = 0;
void addvec(int *x,int *y,int *z, int n)
{
int i;
addcnt++;
for(i = 0; i < n; i++){
z[i] = x[i] + y[i];
}
}
由上圖得知:
在代碼段中,編譯器創(chuàng)建了一個(gè)PLT表,PLT包括多個(gè)表項(xiàng),每個(gè)表項(xiàng)包括幾條指令,PPLT[0]和PLT[1]是內(nèi)置的表項(xiàng),PLT[0】用于調(diào)用動(dòng)態(tài)鏈接器,然后重定位某個(gè)函數(shù)引用的地址,PLT[1]用于跳轉(zhuǎn)到__libc_start_main函數(shù)執(zhí)行初始化工作,這個(gè)函數(shù)調(diào)用了用戶程序的main函數(shù),從PLT[2]開始為函數(shù)引用的表項(xiàng),有多少個(gè)函數(shù)引用就有多少個(gè)表項(xiàng),例如PLT[2]為第一個(gè)函數(shù)引用的表項(xiàng),PLT[3]為第三個(gè)函數(shù)引用的表項(xiàng),以此類推。
在數(shù)據(jù)段中,編譯器創(chuàng)建為了GOT表,GOT表包括多個(gè)表項(xiàng),每個(gè)表項(xiàng)里存儲(chǔ)一個(gè)地址,GOT[0]~GOT[3]是內(nèi)置的表項(xiàng),從GOT[4]開始,每個(gè)函數(shù)一個(gè)表項(xiàng),例如GOT[4]是第一個(gè)函數(shù)引用的表項(xiàng),GOT[5]為第二個(gè)函數(shù)引用的表項(xiàng),編譯器生成可執(zhí)行文件時(shí),GOT[4]中存儲(chǔ)的地址總是指向了PLT[2]表項(xiàng)的第二條指令,GOT[5]中存儲(chǔ)的地址總是指向了PLT[3]表項(xiàng)的第二條指令,因此類推。
以GOT[4]為例,GOT[4]為函數(shù)引用addvec的表項(xiàng)。
當(dāng)一個(gè)目標(biāo)文件第一次調(diào)用addvec時(shí),經(jīng)歷了5個(gè)步驟:
1.執(zhí)行指令call 0x4005c0。0x4005c0為PLT[2]表項(xiàng)的地址,這個(gè)地址是個(gè)相對(duì)地址,與內(nèi)存無關(guān)的,生成可執(zhí)行文件時(shí),就已經(jīng)確定了。
2.跳轉(zhuǎn)到PLT[2]表項(xiàng)即0x4005c0地址處,開始執(zhí)行PLT[2]表項(xiàng)的指令,PLT[2]表項(xiàng)的第一條指令就是跳轉(zhuǎn)到GOT[4]指向的地址,這個(gè)地址在第一次調(diào)用時(shí),剛好是PLT[2]表項(xiàng)的第二條指令地址。
3.跳轉(zhuǎn)到PLT[2]表項(xiàng)的第二條指令后,將函數(shù)引用的編號(hào)這里是0x01入棧,然后跳轉(zhuǎn)到了PLT[0]表項(xiàng)。
4.開始執(zhí)行PLT[0]表項(xiàng)的指令,第一條指令是將重定位表的地址入棧,然后調(diào)用動(dòng)態(tài)鏈接器。
5.動(dòng)態(tài)鏈接器利用傳入的重定位表和函數(shù)引用編號(hào)對(duì)函數(shù)進(jìn)行重新定位,找到了函數(shù)引用對(duì)應(yīng)的內(nèi)存地址,然后更新到GOT[4]。
當(dāng)一個(gè)目標(biāo)文件第二次調(diào)用addvec函數(shù)時(shí),經(jīng)歷了2個(gè)步驟:
1.call 0x4005c0。0x4005c0為函數(shù)引用的表項(xiàng)即PLT[2]的地址。
2.跳轉(zhuǎn)到PLT[2]表項(xiàng),開始執(zhí)行PLT[2]表項(xiàng)的指令,PLT[2]表項(xiàng)的第一條指令就是跳轉(zhuǎn)到GOT[4]指向的地址,由于GOT[4]剛好是函數(shù)引用的地址,直接調(diào)用函數(shù)了,后面的步驟就不需要了。
發(fā)表評(píng)論
請(qǐng)輸入評(píng)論內(nèi)容...
請(qǐng)輸入評(píng)論/評(píng)論長(zhǎng)度6~500個(gè)字
最新活動(dòng)更多
-
即日-11.13立即報(bào)名>>> 【在線會(huì)議】多物理場(chǎng)仿真助跑新能源汽車
-
11月28日立即報(bào)名>>> 2024工程師系列—工業(yè)電子技術(shù)在線會(huì)議
-
12月19日立即報(bào)名>> 【線下會(huì)議】OFweek 2024(第九屆)物聯(lián)網(wǎng)產(chǎn)業(yè)大會(huì)
-
即日-12.26火熱報(bào)名中>> OFweek2024中國(guó)智造CIO在線峰會(huì)
-
即日-2025.8.1立即下載>> 《2024智能制造產(chǎn)業(yè)高端化、智能化、綠色化發(fā)展藍(lán)皮書》
-
精彩回顧立即查看>> 【限時(shí)免費(fèi)下載】TE暖通空調(diào)系統(tǒng)高效可靠的組件解決方案
推薦專題
- 1 【一周車話】沒有方向盤和踏板的車,你敢坐嗎?
- 2 特斯拉發(fā)布無人駕駛車,還未迎來“Chatgpt時(shí)刻”
- 3 特斯拉股價(jià)大跌15%:Robotaxi離落地還差一個(gè)蘿卜快跑
- 4 馬斯克給的“驚喜”夠嗎?
- 5 打完“價(jià)格戰(zhàn)”,大模型還要比什么?
- 6 馬斯克致敬“國(guó)產(chǎn)蘿卜”?
- 7 神經(jīng)網(wǎng)絡(luò),誰是盈利最強(qiáng)企業(yè)?
- 8 比蘋果偉大100倍!真正改寫人類歷史的智能產(chǎn)品降臨
- 9 諾獎(jiǎng)進(jìn)入“AI時(shí)代”,人類何去何從?
- 10 Open AI融資后成萬億獨(dú)角獸,AI人才之爭(zhēng)開啟
- 高級(jí)軟件工程師 廣東省/深圳市
- 自動(dòng)化高級(jí)工程師 廣東省/深圳市
- 光器件研發(fā)工程師 福建省/福州市
- 銷售總監(jiān)(光器件) 北京市/海淀區(qū)
- 激光器高級(jí)銷售經(jīng)理 上海市/虹口區(qū)
- 光器件物理工程師 北京市/海淀區(qū)
- 激光研發(fā)工程師 北京市/昌平區(qū)
- 技術(shù)專家 廣東省/江門市
- 封裝工程師 北京市/海淀區(qū)
- 結(jié)構(gòu)工程師 廣東省/深圳市