yukke_longriders

ぼちぼちがんばる

urllib.request と BeautifulSoup を使った Python で ウェブスクレイピング

ウェブスクレイピングをする機会があり、やったことを忘れないように整理しておきます。
環境 Python 3.6.0 |Anaconda 4.3.0 (64-bit)|

気象庁のページから気象情報を抽出しようと思います。

import urllib.request
from bs4 import BeautifulSoup

url = 'http://www.data.jma.go.jp/obd/stats/etrn/view/hourly_s1.php? \
    prec_no=44&block_no=47662&year=2017&month=3&day=30'
with urllib.request.urlopen(url) as response:
    html = response.read()
soup = BeautifulSoup(html, 'lxml')

これで読み込みは完了です。

次に、ページにある表の12時の気温、天気の情報を取り出します。
tableタグから絞っていきます。

table = soup.find_all('table')

tableタグの中身がそれぞれリストに格納されます。正確にはリストオブジェクトではなくとなりますが、出力結果もリストと変わらないのででリストと同じようにとらえています。
オフセットを指定して目的の表を探してもいいのですが、結構大変なのでclassまたはid属性で条件を絞ります。
今回の例ではclass属性値が'data2_s'を持つものが2つあるので`id`の属性値を指定します。

table = soup.find_all('table', id='tablefix1')[0]

注意ですが、classの属性名で絞る場合には、予約語と被るので`class_`とアンダーバーを追加する必要があります。
tr、tdタグも同様に。

tds = table.find_all('tr')[13].find_all('td')
tds = [tds[i] for i in [0, 14, 15]]

これで12時の時間、気温、天気のみのtdタグを選択できました。
確認してみます。

from pprint import pprint
pprint(tds)
[<td style="white-space:nowrap">12</td>,
 <td class="data_0_0">16.5</td>,
 <td class="data_0_0"><img alt="快晴" src="../../data/image/tenki/large/F89F.gif"/></td>]

ページと見比べてみると確かに12時の気温と天気まで絞れています。

タグの要素の中身はインスタンス変数textへのアクセスで取り出せます。

print(tds[1].text)
16.5

天気の'快晴'という情報を得るには、imgタグのaltの属性値を取り出します。
findでimgタグを取り出し辞書型としてアクセスします。

print(tds[2].find('img')['alt'])
快晴

これで簡単なものは大体足りると思います。

Python を使って Twitter に投稿するよ

なんかしようかなといろいろ見ていたら学科の人がTiwtterで課題や試験情報を知らせるBOTを作っていたのを思い出したので、似たようなことをやろうと思いました。
とはいえ、自分には限られた知識しかないので簡単なものから始めていきます。
いくつかの言語でできるみたいですが、Pythonは多少知っているのでこれでいきたいと思います。

導入

環境は Ubuntu 14.04.5 LTS の64bit です。
まず専用のアカウントを作ります。
そのあとに開発者用の登録等があるのですが、ここを参考にしました。

の4つの情報はどこかにコピペして保存しておいてください。

次にライブラリのTweepyをインストールします。

$ pip install tweepy

で完了です。
エラーが出たら、足りないものを導入してください。

つぶやく

最初は「初めての投稿」とつぶやいてみます。
post.py

#!/usr/bin/python
# coding: utf-8
import tweepy

#Authorization
consumer_key = "xxxxx"
consumer_secret = "xxxxx"
access_token = "xxxxx"
access_token_secret = "xxxxx"
auth = OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
api = tweepy.API(auth)

#Tweet
api.update_status('初めての投稿')

これで

$ python post.py

を実行すれば「初めての投稿」とつぶやかれています。

改良する

これではつぶやくたびに編集しなければなりません。なので標準入力から自由につぶやけるようにします。
post1.py

#!/usr/bin/python
# coding: utf-8
from tweepy import *

#Authorization
f = open('config.txt')
data = f.read()
f.close()
info = data.split('\n')

def get_oauth():
    consumer_key = info[0]
    consumer_secret = info[1]
    access_token = info[2]
    access_token_secret = info[3]
    auth = OAuthHandler(consumer_key, consumer_secret)
    auth.set_access_token(access_token, access_token_secret)
    return auth

def get_tweet():
    print "tweet"
    tweet = raw_input('>> ')
    return tweet

auth = get_oauth()
api = API(auth)
tweet = get_tweet()
api.update_status(tweet)

毎回4つの認証情報をコピペするのは面倒です。
他のファイルにconfig.txtで保存してそこから参照します。

$ python post1.py
tweet
>> 

これで自由につぶやけるようになりました!

参考サイトリスト

GASの基本事項 GAS CapmNetwork

OSの基礎 0から作るOS開発

int割り込み命令 Interrupt Jump Table

アーキテクチャ X86アセンブラ/x86アーキテクチャ - Wikibooks

LinuxでOS自作入門 3日目 (2)

この記事はIS17er Advent Calendar 2016 - Adventarの16日目の記事として書かれました。


今回からOS本体を書き始めます。
とりあえず3日目の真っ黒の画面を出すというところまでできました。今回は苦戦しました。本ではやんわりと書かれていたことの意味を理解できていないことが原因です。
それでも今回分は一つの山場だったと思います。これから少しでも楽になると信じたい。それと写し間違えないように
この方にたどり着いたおかげで助かりました。takeisa memo: [OS作成]30日でできる!OS自作入門 3日目 (4)

はじめに

今回の大まかな流れです。
前回ブートセクタが完成したので、そこへ別に記述したプログラムと組み合わせてOSを作ってみようというものです。今後C言語のプログラムと組み合わせるための導入になります。
ブートセクタに真っ黒の画面を出力するプログラム(今回はこれがOS本体です)のアドレスへjmpする命令を書き足し、ブートセクタから生成されたOSイメージにそのプログラム(OS本体)を保存します。(この保存というのはこの単にファイルををwindows のようなOSのファイルに保存という意味です。)

とりあえずやってみる

本では最初に何もしないプログラムを作っていろいろ検証しているのですが、僕自身はとりあえずやってみようと思い後回しにしたので今の段階ではまだできていません。気になる方は上リンクのブログを参考にしてみてください。
本に書いてあることをそのまま利用します。
空の状態のディスクに対しファイルを保存してみると…

  • ファイル名は 0x002600 以降に入る
  • ファイルの中身は 0x004200 以降に入る

ということがわかります。ブートセクタの先頭アドレスは 0x8000 なので、 0x8000 + 0x4200 = 0xc200 にOS本体の中身が保存されていることがわかります。
まずOS本体になる真っ黒画面生成プログラムです。
os.s

.code16
.text
movb    $0x00, %ah
movb    $0x13, %al
int     $0x10
fin:
hlt
jmp   fin

このプログラムが 0xc200 に読み込まれるようにするためにリンカスクリプトを作ります。
os_lnk.ls

OUTPUT_FORMAT("binary")

SECTIONS {
	. = 0xc200;
	.text :{*(.text)}
}


ブートセクタです。
変更点は、fin:があった位置に jmp の命令が追加され、fin: は最後の方に移動しています。(ファイル名は付属のday3のディレクトリの英字に対応させています。)jmpで飛ぶアドレスには$は必要ないです。
ipl3g.ls

.text
.code16
jmp     entry
.byte   0x90
.ascii  "HELLOIPL" # ブートセクタの名前(8byte)
.word   512 # 1セクタの大きさ(512固定) (16 * 512bit?)
.byte   1 # クラスタのセクタ数(1固定)
.word   1 # FAT開始のセクタ
.byte   2 # FATの個数(2固定)
.word   224 # ルートディレクトリ領域のエントリ数 (224推奨)
.word   2880 # ドライブのセクタ数(2880固定)
.byte   0xf0 # メディアのタイプ(0xf0固定)
.word   9 # FAT領域のセクタ数(9固定)
.word   18 # 1トラックのセクタ数(18固定)
.word   2 # ヘッドの数(2固定)
.int    0 # パーティション数(未使用0)
.int    2880 # ドライブのセクタ数
.byte   0, 0, 0x29 # ?
.int    0xffffffff # ボリュームシリアル番号
.ascii  "HELLO-OS   " # ディスクの名前(11byte)
.ascii  "FAT12   " # フォーマットの名前(8byte)
.skip   18, 0x00 # 18byte空ける

# プログラム本体
entry:
movw	$0, %ax # レジスタの初期化
movw	%ax, %ss
movw	$0x7c00, %sp
movw	%ax, %ds

# ディスクを読む
movw	$0x0820, %ax
movw	%ax, %es
movb	$0, %ch # シリンダ0
movb	$0, %dh # ヘッド0
movb	$2, %cl # セクタ2

readloop:
movw    $0, %si # 失敗回数のカウンター

retry:
movb	$0x02, %ah # ディスクの読み込み
movb	$1, %al # 1セクタ
movw	$0, %bx
movb	$0x00, %dl # Aドライブ
int 	$0x13 # ディスクBIOS呼び出し
jnc     next # エラーがなければnextへ
addw    $1, %si
cmpw    $5, %si # 
jae     error # ( 5 <= si) then error

movb    $0x00, %ah
movb    $0x00, %dl # Aドライブ
int     $0x13 # ドライブのリセット
jmp     retry

next:
movw    %es, %ax # アドレスを0x0020 進める
add    $0x20, %ax
movw    %ax, %es 
# addw    $0x512, %bx
add    $1, %cl # セクタ
cmp    $18, %cl # 
jbe     readloop  # (18 >= cl) then readloop
movb    $1, %cl
add   $1, %dh # ヘッド
cmp    $2, %dh
jb      readloop # (2 > dh) then readloop
movb    $0, %dh
add    $1, %ch # シリンダ
cmp    $10, %ch
jb      readloop # (10 > ch) then readloop

movb    %ch, (0x0ff0) # iplがどこまで読んだかをメモ
jmp     0xc200

error:
movw	$msg, %si

putloop:
movb	(%si), %al
addw	$1, %si
cmpb	$0, %al
je  	fin
movb	$0x0e, %ah # 一文字表示ファンクション
movw	$15, %bx # カラーコード
int 	$0x10 # ビデオBIOS呼び出し
jmp 	putloop

fin:
hlt
jmp     fin

msg:
.byte	0x0a, 0x0a # 改行 改行(ascii)
.ascii	"load error" # 出力
.byte	0x0a # 改行
.byte	0

これらを利用して実行していきます。

$ gcc ipl3g.s -nostdlib -Tipl.ls -o ipl.bin
$ mformat -f 1440 -B ipl.bin -C -i os.img
$ gcc -nostdlib os.s -Tos_lnk.ls -o os.sys
$ mcopy os.sys -i os.img ::

まず前回同様、ipl3g.s からバイナリipl.binを作り、OSイメージファイルos.imgを作ります。
そして今回作ったOS本体 os.s からシステムファイル os.sys を生成します。
最後にシステムファイルをOSイメージファイルに保存します。
ここで出てくる formatmcopy は mtoolsコマンド でMS-DOSフォーマットのフロッピーディスクを操作するコマンドです。
mcopy でファイルを保存します。
以上で完成したOSイメージを今までと同じように実行します。

$ qemu-system-x86_64 -fda os.img

すると
f:id:yukke42:20161216043036p:plain
やりました!真っ黒な画面が出てきました。
今日の分はこれで終わりです。

最後に

今日はいろいろ悩みました。OSイメージを"OS" ではなくプログラムの何かとぼんやりととらえていたためにこれに保存するという意味が全く呑み込めませんでした。そのおかげで少しすっきりした気がします。
今までは16bitのOSでしたが、次は32bitのOSになります。説明は後回しに100行くらい追加されているようです…3日目はまだまだ続きます…

LinuxでOS自作入門 3日目 (1)

今回から1日分が長くなりそうなのでいくつかに分割します。

余談ですが、qemuをつかっていると突然マウス、キーボードが反応しなくなりました。これは単にエミュレータ側の操作に切り替わっただけなのでCtrl + Alt で操作が戻ります。とはいえ、急にPCが反応しなくなったので焦りました…

セクタを読み込む

前回まではブートセクタのみを読み込んでいましたが、3日目は読み込む領域を増やしていきます。

まず、フロッピーディスクの基本構造を説明します。これでプログラム本体のコメントの意味がいくつかわかります。
フロッピーディスクは今ではすでに全く見かけないものですが、見た目は四角くて薄いものです。その中にCDのような円盤状の磁気フィルムが入っています。
その円盤を同心円状に80コに分割した領域をシリンダといいます。一番外側を0として79まであります。さらにその同心円状の領域を18コに分割し、その一つ一つをセクタといいます。セクタは1から18までの番号です。フィルムを読み込むためにヘッドがあるのですが、両面にあるので表を0、裏を1とします。
今はデータを保存するのにSSDやHDDを利用しますが、HDDの基本構造はこれと似ています。

最初はブートセクタの次のセクタを読み込むプログラムです。
ほぼ前回と同じで、entry:の後半追加とのメッセージ内容の変更です。
ipl3a.s

.text
.code16
jmp entry
.byte 0x90
.ascii "HELLOIPL" # ブートセクタの名前(8byte)
.word 512 # 1セクタの大きさ(512固定) (16 * 512bit?)
.byte 1 # クラスタのセクタ数(1固定)
.word 1 # FAT開始のセクタ
.byte 2 # FATの個数(2固定)
.word 224 # ルートディレクトリ領域のエントリ数 (224推奨)
.word 2880 # ドライブのセクタ数(2880固定)
.byte 0xf0 # メディアのタイプ(0xf0固定)
.word 9 # FAT領域のセクタ数(9固定)
.word 18 # 1トラックのセクタ数(18固定)
.word 2 # ヘッドの数(2固定)
.int 0 # パーティション数(未使用0)
.int 2880 # ドライブのセクタ数
.byte 0, 0, 0x29 # ?
.int 0xffffffff # ボリュームシリアル番号
.ascii "HELLO-OS   " # ディスクの名前(11byte)
.ascii "FAT12   " # フォーマットの名前(8byte)
.skip 18, 0x00 # 18byte空ける

# プログラム本体
entry:
movw	$0, %ax # レジスタの初期化
movw	%ax, %ss
movw	$0x7c00, %sp
movw	%ax, %ds

movw	$0x0820, %ax
movw	%ax, %es
movb	$0, %ch # シリンダ0
movb	$0, %dh # ヘッド0
movb	$2, %cl # セクタ2

movb	$0x02, %ah # ディスクの読み込み
movb	$1, %al # 1セクタ
movw	$0, %bx
movb	$0x00, %dl # Aドライブ
int 	$0x13 # ディスクBIOS呼び出し
jc  	error

fin:
hlt # 何かあるまでcpuを停止させる
jmp 	fin # 無限ループ

error:
movw	$msg, %si

putloop:
movb	(%si), %al
addw	$1, %si
cmpb	$0, %al
je  	fin
movb	$0x0e, %ah # 一文字表示ファンクション
movw	$15, %bx # カラーコード
int 	$0x10 # ビデオBIOS呼び出し
jmp 	putloop

msg:
.byte	0x0a, 0x0a # 改行 改行(ascii)
.ascii	"error" # 出力
.byte	0x0a # 改行
.byte	0

追加部分の最後の行に jc がありますが、これは "jump if carry" の略です。キャリーフラグが1だったら飛ぶという命令です。
キャリーフラグというのは、EFLAGSレジスタという操作結果やプロセッサの状態を判断するために使われるレジスタのひとつです。EFLAGSレジスタは32bitのレジスタですが、1bit毎にそれぞれの役割で使われるので、状態は0か1の2つです。
このキャリーフラグは計算結果の桁上がりがあるか判断するために使われるのですが、他の用途にも使われるのでここではソフトウェア割り込みが正常に行われたか判断するために利用されています。

その一行前の割り込み命令は、Interrupt Jump Tableがいい資料だと思います。
今回は13のah = 0x02 を見ます。読み込みの時の指定番号だとわかります。その他の説明です(本も参考に)。
al = 処理するセクタ数
ch = シリンダ番号
cl = セクタ番号
dh = ヘッド番号
dl = ドライブ番号
es:bx = バッファアドレス
戻り値 cf = 0 :エラーなし ah = 0, cf = 1 :エラーあり ah にエラーコード

ドライブ番号はいくつかのフロッピーディスクがあったときにどれかを指定する番号です。

バッファアドレス(バッファメモリのアドレスという意味なのでしょう)には二つのレジスタが使われていますが、一つではアドレスを指定できる範囲が小さいので二つ利用します。その際 es * 0x10 + bx の計算結果を利用します。これでも約1MB分までしかアドレス指定できませんが、それでいいそうです。(相対アドレスとか絶対アドレスとかはこれに関係ありそうです。)今回はes = 0x0820, bx = 0 なので0x8200 ~0x831ff番地の 512byte(1セクタ分) が読み込まれます。
セグメントレジスタはメモリ空間のあるひとまとまりの領域の先頭アドレスを指定するのに使われます。配列と同じイメージです。まずセグメントレジスタで大まかなアドレスを指定し、あとは汎用レジスタで細かいアドレスを指定します。こちらは配列の添え字にあたります、なので今回は先頭アドレスを指定することで1セクタ分のメモリの内容が読み込まれます。
putloopラベルの一行目にある(%si) は本来はds にも値を入れなければなりませんが、この値は0なので省略されています。
主なセグメントレジスタです。

  • SS:スタックセグメント。スタックへのポインタ。
  • CS: コードセグメント。コードへのポインタ。
  • DS: データセグメント。データへのポインタ。
  • ES: エクストラセグメント。追加のデータへのポインタ。(ExtraのE)

    追加分の説明は終わったので実行してみます。

    $ make rungas

    今回は正しく動作するとなのも起こりません。
    どこかおかしいと "error" と表示されます。

  • 読み込みエラーの処理

    読み込み失敗時に何度も読み込もうとするのですが、それの回数を制限します。

    ipl3b.s

    .code16
    jmp entry
    .byte 0x90
    .ascii "HELLOIPL" # ブートセクタの名前(8byte)
    .word 512 # 1セクタの大きさ(512固定) (16 * 512bit?)
    .byte 1 # クラスタのセクタ数(1固定)
    .word 1 # FAT開始のセクタ
    .byte 2 # FATの個数(2固定)
    .word 224 # ルートディレクトリ領域のエントリ数 (224推奨)
    .word 2880 # ドライブのセクタ数(2880固定)
    .byte 0xf0 # メディアのタイプ(0xf0固定)
    .word 9 # FAT領域のセクタ数(9固定)
    .word 18 # 1トラックのセクタ数(18固定)
    .word 2 # ヘッドの数(2固定)
    .int 0 # パーティション数(未使用0)
    .int 2880 # ドライブのセクタ数
    .byte 0, 0, 0x29 # ?
    .int 0xffffffff # ボリュームシリアル番号
    .ascii "HELLO-OS   " # ディスクの名前(11byte)
    .ascii "FAT12   " # フォーマットの名前(8byte)
    .skip 18, 0x00 # 18byte空ける
    
    # プログラム本体
    entry:
    movw	$0, %ax # レジスタの初期化
    movw	%ax, %ss
    movw	$0x7c00, %sp
    movw	%ax, %ds
    
    # ディスクを読む
    movw	$0x0820, %ax
    movw	%ax, %es
    movb	$0, %ch # シリンダ0
    movb	$0, %dh # ヘッド0
    movb	$2, %cl # セクタ2
    movw    $0, %si # 失敗回数のカウンター
    
    retry:
    movb	$0x02, %ah # ディスクの読み込み
    movb	$1, %al # 1セクタ
    movw	$0, %bx
    movb	$0x00, %dl # Aドライブ
    int 	$0x13 # ディスクBIOS呼び出し
    jnc     fin # エラーがなければfinへ
    addw    $1, %si # 
    cmpw    $5, %si # 5回失敗でエラー
    jae     error
    movb    $0x00, %ah
    movb    $0x00, %dl # Aドライブ
    int     $0x13 # ドライブのリセット
    jmp     retry
    
    fin:
    hlt # 何かあるまでcpuを停止させる
    jmp 	fin # 無限ループ
    
    error:
    movw	$msg, %si
    
    putloop:
    movb	(%si), %al
    addw	$1, %si
    cmpb	$0, %al
    je  	fin
    movb	$0x0e, %ah # 一文字表示ファンクション
    movw	$15, %bx # カラーコード
    int 	$0x10 # ビデオBIOS呼び出し
    jmp 	putloop
    
    msg:
    .byte	0x0a, 0x0a # 改行 改行(ascii)
    .ascii	"error" # 出力
    .byte	0x0a # 改行
    .byte	0

    ドライブのリセットというところが新しいところです。他は特に無いです。
    今回も表示画面は上と同じです。

    さらにセクタを読み込む

    最初はブートセクタの次のセクタのみを読み込みましたが、その読み込むセクタ数を増やしていきます。18セクタつまり1シリンダ分読み込みます。

    ipl3c.s

    .code16
    jmp entry
    .byte 0x90
    .ascii "HELLOIPL" # ブートセクタの名前(8byte)
    .word 512 # 1セクタの大きさ(512固定) (16 * 512bit?)
    .byte 1 # クラスタのセクタ数(1固定)
    .word 1 # FAT開始のセクタ
    .byte 2 # FATの個数(2固定)
    .word 224 # ルートディレクトリ領域のエントリ数 (224推奨)
    .word 2880 # ドライブのセクタ数(2880固定)
    .byte 0xf0 # メディアのタイプ(0xf0固定)
    .word 9 # FAT領域のセクタ数(9固定)
    .word 18 # 1トラックのセクタ数(18固定)
    .word 2 # ヘッドの数(2固定)
    .int 0 # パーティション数(未使用0)
    .int 2880 # ドライブのセクタ数
    .byte 0, 0, 0x29 # ?
    .int 0xffffffff # ボリュームシリアル番号
    .ascii "HELLO-OS   " # ディスクの名前(11byte)
    .ascii "FAT12   " # フォーマットの名前(8byte)
    .skip 18, 0x00 # 18byte空ける
    
    # プログラム本体
    entry:
    movw	$0, %ax # レジスタの初期化
    movw	%ax, %ss
    movw	$0x7c00, %sp
    movw	%ax, %ds
    
    # ディスクを読む
    movw	$0x0820, %ax
    movw	%ax, %es
    movb	$0, %ch # シリンダ0
    movb	$0, %dh # ヘッド0
    movb	$2, %cl # セクタ2
    
    readloop:
    movw    $0, %si # 失敗回数のカウンター
    
    retry:
    movb	$0x02, %ah #ディスクの読み込み
    movb	$1, %al # 1セクタ
    movw	$0, %bx
    movb	$0x00, %dl # Aドライブ
    int 	$0x13 # ディスクBIOS呼び出し
    jnc     next # エラーがなければnextへ
    addw    $1, %si 
    cmpw    $5, %si # 5回失敗でエラーへ
    jae     error
    movb    $0x00, %ah
    movb    $0x00, %dl # Aドライブ
    int     $0x13 # ドライブのリセット
    jmp     retry
    
    next:
    movw    %es, %ax # アドレスを0x0020 進める
    addw    $0x0020, %ax
    movw    %es, %ax
    # addw    $0x512, %bx
    addb    $1, %cl # セクタ
    cmpb    $18, %cl #
    jbe     readloop  # (18 >= cl) then readloop
    
    fin:
    hlt # 何かあるまでcpuを停止させる
    jmp 	fin # 無限ループ
    
    error:
    movw	$msg, %si
    
    putloop:
    movb	(%si), %al
    addw	$1, %si
    cmpb	$0, %al
    je  	fin
    movb	$0x0e, %ah # 一文字表示ファンクション
    movw	$15, %bx # カラーコード
    int 	$0x10 # ビデオBIOS呼び出し
    jmp 	putloop
    
    msg:
    .byte	0x0a, 0x0a # 改行 改行(ascii)
    .ascii	"error" # 出力
    .byte	0x0a # 改行
    .byte	0

    セクタ番号の cl を1増やし、読み込み番地を指定する es を0x20 進めれば次のセクタを読み込めます。
    next:の最初の3行は esを 一発で0x20増やす方法がないので他のレジスタを経由して計算しています。(実際にやってみるとミスマッチのエラーが出ます。)これは先ほど確認したように 0x20 * 0x10 + 0x00 = 0x200 = 512 なのでこれで1セクタ分アドレスを進めたことになります。
    これは アドレス指定に使う bx を直接増やしても同じです。

    また、17セクタ(ビートセクタ以外)を読み込むのにループにせず al を17にすれば簡単に済みますが、これでは次の場合に問題があるみたいです。

    いくつかのシリンダを読み込む

    今日の分はキリがいいのでこれで最後にします。
    次はシリンダをさらに読み込んでいきます。その際読み込む領域が裏側にいく場合のことも考えます。
    2行目と、next: に追加部分があります。

    ipl3d.s

    .code16
    .equ    cyls, 10
    jmp     entry
    .byte   0x90
    .ascii  "HELLOIPL" # ブートセクタの名前(8byte)
    .word   512 # 1セクタの大きさ(512固定) (16 * 512bit?)
    .byte   1 # クラスタのセクタ数(1固定)
    .word   1 # FAT開始のセクタ
    .byte   2 # FATの個数(2固定)
    .word   224 # ルートディレクトリ領域のエントリ数 (224推奨)
    .word   2880 # ドライブのセクタ数(2880固定)
    .byte   0xf0 # メディアのタイプ(0xf0固定)
    .word   9 # FAT領域のセクタ数(9固定)
    .word   18 # 1トラックのセクタ数(18固定)
    .word   2 # ヘッドの数(2固定)
    .int    0 # パーティション数(未使用0)
    .int    2880 # ドライブのセクタ数
    .byte   0, 0, 0x29 # ?
    .int    0xffffffff # ボリュームシリアル番号
    .ascii  "HELLO-OS   " # ディスクの名前(11byte)
    .ascii  "FAT12   " # フォーマットの名前(8byte)
    .skip 18, 0x00 # 18byte空ける
    
    # プログラム本体
    entry:
    movw	$0, %ax # レジスタの初期化
    movw	%ax, %ss
    movw	$0x7c00, %sp
    movw	%ax, %ds
    
    # ディスクを読む
    movw	$0x0820, %ax
    movw	%ax, %es
    movb	$0, %ch # シリンダ0
    movb	$0, %dh # ヘッド0
    movb	$2, %cl # セクタ2
    
    readloop:
    movw    $0, %si # 失敗回数のカウンター
    
    retry:
    movb	$0x02, %ah # ディスクの読み込み
    movb	$1, %al # 1セクタ
    movw	$0, %bx
    movb	$0x00, %dl # Aドライブ
    int 	$0x13 # ディスクBIOS呼び出し
    jnc     next # エラーがなければnextへ
    addw    $1, %si 
    cmpw    $5, %si # if ( 5 <= si)
    jae     error
    movb    $0x00, %ah
    movb    $0x00, %dl # Aドライブ
    int     $0x13 # ドライブのリセット
    jmp     retry
    
    next:
    movw    %es, %ax # アドレスを0x0020 進める
    addw    $0x0020, %ax
    movw    %es, %ax
    # addw    $0x512, %bx
    addb    $1, %cl # セクタ
    cmpb    $18, %cl # 
    jbe     readloop  # (18 >= cl) then readloop
    movb    $1, %cl
    addb    $1, %dh #ヘッド
    cmpb    $2, %dh 
    jb      readloop # (2 > dh) then readloop
    movb    $0, %dh
    addb    $1, %ch # シリンダ
    cmpb    cyls, %ch 
    jb      readloop # (clys > ch) then readloop
    
    fin:
    hlt # 何かあるまでcpuを停止させる
    jmp 	fin # 無限ループ
    
    error:
    movw	$msg, %si
    
    putloop:
    movb	(%si), %al
    addw	$1, %si
    cmpb	$0, %al
    je  	fin
    movb	$0x0e, %ah # 一文字表示ファンクション
    movw	$15, %bx # カラーコード
    int 	$0x10 # ビデオBIOS呼び出し
    jmp 	putloop
    
    msg:
    .byte	0x0a, 0x0a # 改行 改行(ascii)
    .ascii	"load error" # 出力
    .byte	0x0a # 改行
    .byte	0

    2行目は読み込むシリンダ数を指定するための変数を宣言しています。
    これで実行してエラーがなければ今までと同じ特に何もない画面が表示されます。

    最後に

    これでとりあえず読み込みができるようになり、3日目前半(?)が終わりました。
    続きはとうとうOS本体に取り掛かっていきます。

    LinuxでOS自作入門 2日目

    今回からレジスタなどが出てきて、アセンブラらしくなります。

    そして、NASM に関することコード等はこれからは省きたいと思います。

    アセンブラの基本

    基本文法とレジスタGAS_基本文法 CapmNetworkx86-レジスタ CapmNetworkを参考にしました。
    軽くまとめていきます。

    GASの命令はニーモニックといい、マシン語命令のオペコードとオペランドで扱うデータサイズを指定するオペレージョンサフィックスで構成されています。
    オペレーションサフィックス

    b バイト(8ビット)
    s ショート (16ビット整数)またはsingle(32ビット浮動小数点数)
    w ワード(16ビット)
    l ロング(32ビット整数または64ビット浮動小数点数)
    q クワッド(64ビット)
    となります。(表は引用です。)

    オペランドは引数のことです。

    ニーモニック(オペコード + サフィックス) オペランド

    という形式になります。
    例)

    movw $0, %ax
    addw $1, %si

    また、これを見れば何となく気づくかもしれませんが AT&T構文であるGASはソースオペランド、デスティネーションオペランドという順番になり演算結果は右側のレジスタに格納されます。
    つまりプログラミング言語言語などの代入とは違い

    x = 0

    と演算結果が左側に格納されるというものと逆になるわけです。

    レジスタですが、細かい説明は上の記事がわかりやすいのでそちらにお願いします。
    GAS で使われているAT&T構文はレジスタの前には % をつけ、数字、ラベル名の前には $ をつけます。



    書き換え

    本で出てきたものを順番に、GAS の形式として中心としてまとめます。
    コメントもつけました。
    hellos2.s

    	.code16
    	jmp entry
    	.byte 0x90
    	.ascii "HELLOIPL" # ブートセクタの名前(8byte)
    	.word 512 # 1セクタの大きさ(512固定) 
    	.byte 1 # クラスタのセクタ数(1固定)
    	.word 1 # FAT開始のセクタ
    	.byte 2 # FATの個数(2固定)
    	.word 224 # ルートディレクトリ領域のエントリ数 (224推奨)
    	.word 2880 # ドライブのセクタ数(2880固定)
    	.byte 0xf0 # メディアのタイプ(0xf0固定)
    	.word 9 # FAT領域のセクタ数(9固定)
    	.word 18 # 1トラックのセクタ数(18固定)
    	.word 2 # ヘッドの数(2固定)
    	.int 0 # パーティション数(未使用0)
    	.int 2880 # ドライブのセクタ数
    	.byte 0, 0, 0x29 # ?
    	.int 0xffffffff # ボリュームシリアル番号
    	.ascii "HELLO-OS   " # ディスクの名前(11byte)
    	.ascii "FAT12   " # フォーマットの名前(8byte)
    	.skip 18, 0x00 # 18byte空ける
    
    # プログラム本体
    entry:
    	movw	$0, %ax # ax = 0 レジスタの初期化
    	movw	%ax, %ss # ss = a = 0
    	movw	$0x7c00, %sp # sp = 0x7c00
    	movw	%ax, %ds # ds = ax = 0
    	movw	%ax, %es # es = ax = 0
    
    	movw	$msg, %si # si = &mag
    putloop:
    	movb	(%si), %al # al = &si
    	addw	$1, %si # si += 1
    	cmpb	$0, %al # if (al == 0)
    	je      fin # jump to fin
    	movb	$0x0e, %ah # ah = 0x0e  一文字表示ファンクション
    	movw	$15, %bx # bx = 15    カラーコード
    	int     $0x10 # ビデオBIOS呼び出し
    	jmp	putloop
    fin:
    	hlt # 何かあるまでcpuを停止させる
    	jmp	fin # 無限ループ
    
    # メッセージ部分
    msg:
    	.byte	0x0a, 0x0a # 改行 改行(ascii)
    	.ascii	"Hello, GAS" # 出力
    	.byte	0x0a # 改行
    	.byte	0
    
    	.org	0x1fe #0x1fe分0x00で埋める命令
    
    	.byte	0x55, 0xaa
    
    # 以下はブートセクタ以外の部分の記述
    	.byte	0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    	.org	.+4600
    	.byte	0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    	.org	.+1469432
    
    

    1行目
    まず、このコードでは16bitのレジスタが使われています。なのでそれを指定する必要があります。

    命令

    ORG命令機械語が実行時にPCに読み込まれるメモリを指定します。
    メモリマップにおける 0x00007c00 - 0x00007dff はブートプログラムなどが格納されているというブートセクタが読み込まれるアドレスなのでこの頭の数字になるみたいです。
    どうもGASには対応するものがないようなので外部で指定します。
    yukke42.hatenablog.comで作ったリンカスクリプト
    lnk.ls 後半をに追加します。

    OUTPUT_FORMAT("binary");
    
    IPL_BASE = 0x7C00;
    
    SECTIONS {
      . = IPL_BASE;
    }
    

    2行目
    jmp はラベルのところまで飛びます。(jumpの略)

    25行目
    movw は代入です。ここでは16bit レジスタなのでサフィックスwです。
    33行目
    movb は同じ代入ですが、8bit レジスタを使用しているので サフィックスb です。

    34行目
    addw は文字通り足し算の命令です。 si ← si + 1

    35行目
    cmp
    je
    で1セットの比較の分岐命令となります。c言語でいうと

    if (ax == 0) 
        goto fin;
    

    となります。(goto自体は使ったことないけど…)
    GAS_制御構造 CapmNetwork に他の条件で分岐する場合も載っています。
    (このサイト利用しまくってます。)
    39行目
    int はソフトウェア割り込み命令です。(interruptの略)
    詳細は後日。

    40行目
    hlt はCPUを停止させる命令です。(haltの略)
    fin では無限ループになっているのですが、筆者はCPUをから回しすというのが嫌なのでマイナーな命令ですが追加しているそうです。
    なので、なくても動きます。

    汎用レジスタ

    基本は ax の汎用レジスタを使います。機械語では他のを使うより少し簡単になります。
    bx はメモリ番地計算の基点(ベース)となります。
    cx はカウンタで、回数を数えるのに使います。

    また、ax, bx, ... には上位、下位それぞれ8bitずつを指定できる ah, al, bh, bl, ... というレジスタがあります。

    汎用レジスタのうちほかの4つ(di, sp, si, di) は上位、下位で分けられません。もし分ける必要があるならば、ah 等を経由して操作します。

    セグメントレジスタ

    本編では3日目での説明となります。

    アドレス

    31行目
    $msg とラベルの前に$をつけることでそのアドレスを指定できます。

    33行目
    (%si) とすることで siのアドレスを指定できます。
    このとき16bit(2byte) の値がメモリに格納されているのですが、値は8bit(1byte)毎に格納され上位がアドレスの大きい側となります。
    これをリトルエンディアンといいます。

    これらのようなアドレス指定に使えるレジスタbx, bp, si, di のみでほかは使えません。

    とりあえず一通りの説明は終わりです。

    make rungas

    で実行すればOKです。
    表示は一日目と変わらないので割愛。

    最後に

    読み進めながらやっているので、本を持っていない人にはかなりわかりづらいと思います。。そこは改善していこうと思います。
    本編のコードに書いてあるコメントを追加しましたが、聞いたことだけあるくらいの用語が大量にあります。一応調べはしたのですが、まとめるのはまたそのうち…
    とりあえず

    • リンカスクリプト
    • ソフトウェア割り込み
    • セクタ等の関係 これは3日目の(1)に説明あります。

    は優先度高めです。

    ~追記(2016/12/15)~
    今まではブートセクタ以降の記述も一つのファイルに書いていたのですが、これではファイルサイズが大きくなりブートセクタに入りきらなくなるのでここの記述を外部に任せます。そのためにMakefileの内容も書き換えます。
    mformatコマンドはOSイメージを作るときに1,440KB ディスクを全て埋めます。これ最後にあったブートセクタ以降の記述が必要なくなります。
    そうすることで、読み込みのみのファイルが今日作ることができます。
    それに伴ってMakefileを変更し、の書き方も少し変えました。run から一本道でたどっていけるようにしました。(分割するのとどっちがいいかは好みでしょうか。)
    Makefile

    GAS = ipl3a.s
    IMG = ipl.img
    IPL = ipl.bin
    
    ipl.bin: $(GAS)
    	gcc $(GAS) -nostdlib -T lnk.ls -o $(IPL)
    
    ipl.img: $(IPL)
    	mformat -f 1440 -C -B $(IPL) -i $(IMG)
    	rm $(IPL)
    
    run: $(IMG)
    	qemu-system-x86_64 -fda $(IMG)
    	rm $(IMG)
    

    LinuxでOS自作入門 1日目

    始める前に

    本編ではいろいろ準備があるが、特に関係ないので飛ばす。

    本体のプログラムを作っていくのだが、いきなりバイナリで数字をひたすら打ち込む。のも嫌なのでスキップ。

    まず試しに

    ここで nask を NASM に書き直していきます。これは簡単で4か所あり RESB (数字)TIMES (数字) DB 0 にすればいいです。
    RESB は NASM では推奨されいていないようです。
    それと最後の方にある 0x1fe-$0x1fe-($-$$) に変えます。
    nask では$はこの行が先頭から何バイト目かを返す変数となります。

    helloos1.nasm

    DB    0xeb, 0x4e, 0x90
    DB    "HELLOIPL"
    DW    512
    DB    1
    DW    1
    DB    2
    DW    224
    DW    2880
    DB    0xf0
    DW    9
    DW    18
    DW    2
    DD    0
    DD    2880
    DB    0,0,0x29
    DD    0xffffffff
    DB    "HELLO-OS   "
    DB    "FAT12   "
    TIMES 18 DB    0
    
    ;プログラム本体
    DB    0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
    DB    0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
    DB    0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
    DB    0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
    DB    0xee, 0xf4, 0xeb, 0xfd
    
    ; メッセージ部分
    DB    0x0a, 0x0a
    DB    "hello, NASM"
    DB    0x0a
    DB    0
    
    TIMES 0x1fe-($-$$) DB    0
    
    DB    0x55, 0xaa
    
    ; 以下はブートセクタ以外の部分の記述
    DB    0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    TIMES 4600 DB    0
    DB    0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    TIMES 1469432 DB    0

    これをOSイメージにして実行します。
    -fdaフロッピーディスクとして実行するオプションです。

    $ nasm helloos1.asm -o helloos1asm.img
    $ qemu-system-x86_64 -fda helloos1asm.img

    f:id:yukke42:20161212233003p:plain
    (メッセージは変えてみた)

    本編

    ここから GAS に書き直していきます。
    まず、 BW DD をそれぞれ .word .intにします。それぞれは、16bit(2byte),32bit(4byte) の値を格納しその領域を確保する命令。
    一番多い DB は、次に続くものが数字なら .byte に、文字列なら .ascii にします。これらも8bit(1byte), 文字列に対し同様に動作する命令です。
    最後は0で埋める命令の TIMESskip にし、追加した DB 0は消します。そして 最後の方にある -($-$$)も消します。 数字の後に、0 で埋めるということを明記します。

    そうするとこうなります。
    helloos1.s

    .byte   0xeb, 0x4e, 0x90
    .ascii  "HELLOIPL"
    .word   512
    .byte   1
    .word   1
    .byte   2
    .word   224
    .word   2880
    .byte   0xf0
    .word   9
    .word   18
    .word   2
    .int    0
    .int    2880
    .byte   0,0,0x29
    .int    0xffffffff
    .ascii  "HELLO-OS   "
    .ascii  "FAT12   "
    .skip    18, 0x00
    
    # プログラム本体
    .byte   0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
    .byte   0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
    .byte   0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
    .byte   0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
    .byte   0xee, 0xf4, 0xeb, 0xfd
    
    # メッセージ部分
    .byte   0x0a, 0x0a
    .ascii  "hello, GAS"
    .byte   0x0a
    .byte   0
    
    .org    0x1fe # 0x1fe 分0x00で埋める命令
    
    .byte   0x55, 0xaa
    
    # 以下はブートセクタ以外の部分の記述
    .byte   0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    .skip   4600, 0x00
    .byte   0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    .skip   1469432, 0x00
    

    同様にOSイメージにして実行します。

    $ gcc helloos1.s -nostdlib -Tlnk.ls -o helloos1s.img
    $ qemu-system-x86_64 -fda helloos1s.img

    (これのオプションがいる理由はわかりません..)
    〜追記(2016/12/14)〜
    -nostdlib は標準ライブラリをリンクしないということだそうです。

    f:id:yukke42:20161212233018p:plain

    最後に

    環境を整えるのに知らないことが多すぎます。なので本編ではさらっと終わっているのに時間は思ったよりかかりました。
    時間はかなりかかりそうですが少しずつ進めていきたいと思います。

    補足

    これは2日目の先取りとなるのですが、毎回OSイメージを作成&実行するのを入力して試行錯誤するのはめんどうになります。
    そこで Makefile というものを利用します。

    Makefile で保存します。

    helloos1asm.img:
    	nasm helloos1.asm -o helloos1asm.img
    
    runnasm:
    	make helloos1asm.img
    	qemu-system-x86_64 -fda helloos1asm.img
    	rm helloos1asm.img
    
    helloos1s.img:
    	gcc helloos1.s -nostdlib -Tlnk.ls -o helloos1s.img
    
    rungas:
    	make helloos1s.img
    	qemu-system-x86_64 -fda helloos1s.img
    	rm helloos1s.img
    

    〜追記(2016/12/13)〜
    Make について誤解していたようですね。(今でも微妙ですが)書き直します。

    IMG = helloos2s.img
    GAS = helloos2.s
    
    img:
    	gcc $(GAS) -nostdlib -T lnk.ls -o $(IMG)
    
    rungas:
    	make img
    	qemu-system-x86_64 -fda $(IMG)
    	rm $(IMG)

    ファイル名の書き直す箇所が少なくなります。
    〜追記終わり〜

    こうすれば

    $ make rungas

    とか

    $ make runnasm

    と入力すればアセンブリファイルからイメージ作成、実行まで一度にできるようになります。
    ファイル名を変更すれば毎回利用できます。
    (.img は残っているとうまく上書きされずにそのまま実行してしまうみたいなので、一応毎回消しときます。)

    ~追記(2016/12/15)~
    最後のページにあった記述に関することで、今日の分のようにそのまま命令で書いてもいいのですが、リンカスクリプトで記述もできるようなので載せときます。(3日目以降の分から変更されています。)
    main関数だよと宣言する.textが2行目に追加されています。
    lnk.ls

    OUTPUT_FORMAT("binary");
    IPLBASE = 0x7c00;
    
    SECTIONS {
            . = IPLBASE;
            .text :{*(.text)}
            . = IPLBASE + 0x1fe;
            .sign : {SHORT(0xaa55)}
    }