動作確認
1970年からNTPから時間を取得した時点までの秒数を表示します。
その後date命令を用いて何年何月何日何時何分何秒の形式で表示します。
私は中国に住んでいるのですが世界標準時間で出力された以下の2025-05-24 11:59:31に8時間を足すと丁度一致しました。日本だと9時間足す必要があります。
ubuntu@ubuntu:~/kaihatsu/ntp$ nasm -f elf64 ntp.asm -o ntp.o
ubuntu@ubuntu:~/kaihatsu/ntp$ ld ntp.o -o ntp
ubuntu@ubuntu:~/kaihatsu/ntp$ ./ntp
1748087971ubuntu@ubuntu:~/kaihatsu/ntp$ date -ud @1748087971 +"%Y-%m-%d %H:%M:%S UTC"
2025-05-24 11:59:31 UTC
ubuntu@ubuntu:~/kaihatsu/ntp$
予備知識
NTPタイムスタンプ
NTP(Network Time Protocol)のタイムスタンプは、64ビットの固定小数点数形式で表現されます。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Seconds |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Fraction(小数部 1/2^32秒単位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
上位32ビット(Seconds):
1900年1月1日00:00:00 UTCからの経過秒数(秒単位)を表します。
下位32ビット(Fraction):※今回未使用
秒未満の時間を1/232秒単位で表します。
(例: 0x80000000 = 0.5秒)
宛先情報の構造体の構造
; sockaddr_in構造体(IPv4用)
struc sockaddr_in
.sin_family: resw 1 ; AF_INET = 2 IPv4用は2固定
.sin_port: resw 1 ; ポート番号(リトルエンディアン)
.sin_addr: resd 1 ; IPv4アドレス(4バイト)
.sin_zero: resb 8 ; 常に0埋め
endstruc
システムコールの引数及び返り値(C言語風)
1. SYS_SOCKET(ソケット作成)
#include <sys/socket.h>
/**
* ソケットを作成する(64ビットLinux用)
* @param domain 通信ドメイン(32ビット整数: AF_INET=2)
* @param type ソケットタイプ(32ビット整数: SOCK_DGRAM=2)
* @param protocol プロトコル(32ビット整数: 0=自動選択)
* @return 成功: 64ビットファイルディスクリプタ(正の整数)
* 失敗: 64ビットエラーコード(-1)
*/
int64_t socket(int32_t domain, int32_t type, int32_t protocol);
2. SYS_SENDTO(データ送信)
#include <sys/socket.h>
/**
* UDPでデータを送信する(64ビットLinux用)
* @param sockfd 64ビットファイルディスクリプタ
* @param buf 送信データの64ビットポインタ
* @param len 64ビット符号なし整数(データサイズ)
* @param flags 32ビット整数(フラグ: 通常0)
* @param dest_addr 宛先アドレスの64ビットポインタ(sockaddr_in*)
* @param addrlen 32ビット整数(アドレス構造体のサイズ: 16)
* @return 成功: 送信したバイト数(64ビット符号付き整数)
* 失敗: 64ビットエラーコード(-1)
*/
int64_t sendto(
int64_t sockfd,
const void *buf, // 64ビットアドレス
uint64_t len, // 64ビット
int32_t flags, // 32ビット(下位32ビット使用)
const struct sockaddr *dest_addr, // 64ビットアドレス
uint32_t addrlen // 32ビット(下位32ビット使用)
);
3. SYS_RECVFROM(データ受信)
#include <sys/socket.h>
/**
* UDPでデータを受信する(64ビットLinux用)
* @param sockfd 64ビットファイルディスクリプタ
* @param buf 受信バッファの64ビットポインタ
* @param len 64ビット符号なし整数(バッファサイズ)
* @param flags 32ビット整数(フラグ: 通常0)
* @param src_addr 送信元アドレスの64ビットポインタ(NULL可)
* @param addrlen 送信元アドレスサイズの64ビットポインタ(NULL可)
* @return 成功: 受信したバイト数(64ビット符号付き整数)
* 失敗: 64ビットエラーコード(-1)
*/
int64_t recvfrom(
int64_t sockfd,
void *buf, // 64ビットアドレス
uint64_t len, // 64ビット
int32_t flags, // 32ビット(下位32ビット使用)
struct sockaddr *src_addr, // 64ビットアドレス(NULL可能)
uint32_t *addrlen // 64ビットアドレス(NULL可能)
);
検証コード
section .data
; NTPサーバー情報設定(time.nist.gov)
ntp_server: db 129,6,15,28 ; 接続先サーバー(NTP)のIPアドレス
port: dw 0x7B00 ; UDPポート123(リトルエンディアン 0x7B00)
; NTPリクエストパケットの初期化
ntp_packet: db 0x1B ; [重要] フラグ設定:LI=0(正常), VN=3(バージョン3), Mode=3(クライアントモード)
times 47 db 0 ; 48バイトのNTPパケット形式を満たすため0埋め
section .text
global _start
; Linuxシステムコール番号
%define SYS_SOCKET 41
%define SYS_SENDTO 44
%define SYS_RECVFROM 45
%define SYS_CLOSE 3
%define SYS_EXIT 60
%define SYS_WRITE 1
_start:
; [ソケット作成] NTPサーバーと通信するためのUDPソケットを作成
mov rax, SYS_SOCKET
mov rdi, 2 ; AF_INET(IPv4通信を指定)
mov rsi, 2 ; SOCK_DGRAM(UDP通信を指定)
mov rdx, 0 ; プロトコル自動選択(通常UDPは0)
syscall
cmp rax, 0 ; エラーチェック:負の値は失敗
jl error
mov [fd], rax ; 作成したソケットを保存
; [宛先設定] 接続先サーバーのアドレス情報を構造体に設定
mov word [sockaddr], 2 ; sin_family = AF_INET(IPv4)
mov ax, [port] ; ポート番号を設定
mov word [sockaddr+2], ax ; sin_port フィールド
mov eax, [ntp_server] ; IPアドレスを設定(4バイト)
mov dword [sockaddr+4], eax ; sin_addr フィールド
; [リクエスト送信] NTPサーバーに時刻取得要求を送信
mov rax, SYS_SENDTO
mov rdi, [fd] ; 使用するソケットを指定
lea rsi, [ntp_packet] ; 送信データ(事前に準備したNTPリクエスト)
mov rdx, 48 ; NTPパケットの標準サイズ(48バイト)
mov r10, 0 ; フラグなし(通常は0)
lea r8, [sockaddr] ; 宛先アドレス構造体のポインタ
mov r9, 16 ; アドレス構造体のサイズ(sockaddr_inは16バイト)
syscall
cmp rax, 48 ; 正常に48バイト送信されたか確認
jne error
; [レスポンス受信] サーバーからの時刻情報を受信
mov rax, SYS_RECVFROM
mov rdi, [fd] ; 同じソケットを使用
lea rsi, [recv_buf] ; 受信データ格納先バッファ
mov rdx, 48 ; 受信予定データサイズ(NTPレスポンスも48バイト)
mov r10, 0 ; フラグなし
mov r8, 0 ; 送信元アドレス情報不要(NULL指定)
mov r9, 0 ; アドレスサイズ情報不要(NULL指定)
syscall
cmp rax, 48 ; 48バイト受信したか確認
jne error
; [時刻データ抽出] 受信データをeaxへ格納
mov eax, [recv_buf + 32] ; タイムスタンプ上位32ビット(NTPフォーマット)
mov ebx, [recv_buf + 36] ; タイムスタンプ下位32ビット(今回は未使用)
bswap eax ; ビッグエンディアン→リトルエンディアン変換
sub eax, 2208988800 ; NTP時間(1900年起点)→UNIX時間(1970年起点)に変換
; 差は70年分(2208988800秒)
; (注)ここでeaxにUNIX時間が格納される
; [時刻データ表示] 数値をASCII文字列に変換して出力
mov edi, output_buf ; 出力バッファのアドレス
call int_to_ascii ; 数値→ASCII変換関数
mov rax, SYS_WRITE ; writeシステムコール番号設定
mov rdi, 1 ; 標準出力のファイルディスクリプタ
mov rsi, output_buf ; 出力データのポインタ
mov rdx, 10 ; 出力サイズ(10バイト)
syscall
; [ソケット解放] 通信が完了したのでソケットを閉じる
mov rax, SYS_CLOSE
mov rdi, [fd]
syscall
; プログラム正常終了
mov rdi, 0
jmp exit
error:
; エラー発生時の処理
mov rdi, 1 ; 終了1(エラー)
exit:
; システム終了処理
mov rax, SYS_EXIT
syscall
; 数値→ASCII変換関数(32bit符号なし整数用)
int_to_ascii:
mov ecx, 10 ; 除数
mov ebx, 10 ; カウンタ(10桁)
.convert_loop:
dec ebx
xor edx, edx
div ecx ; EDX:EAX / ECX
add dl, '0' ; 余りをASCIIに変換
mov [edi + ebx], dl
test ebx, ebx
jnz .convert_loop
ret
section .bss
fd: resq 1 ; ソケットファイルディスクリプタ保存用
sockaddr: resb 16 ; ソケットアドレス情報格納用(AF_INETの場合16バイト)
recv_buf: resb 48 ; NTPレスポンス受信用バッファ
output_buf: resb 10 ; 追加: 出力用バッファ