42Schoolの課題「libasm」をやるにあたり、学習の過程で得た学びや疑問・思考過程などをメモする。
課題の概要
libasmとは、アセンブラで記述された自作Cライブラリを作る課題である。
ライブラリに含めるのは、strlenやwriteなどの標準的な関数である。なお、名前の最初にはft_を付してft_strlenなどとする。命名以外は標準関数と同じ働きをする。
また、以下の要件がある。
- Makefileを使って、.sファイルを
nasmでアセンブルし、静的ライブラリlibasm.aを作成する。 - また、bit数は64bitとし、Intel Syntaxを用いる。
取り掛かり
家にあった本に沿って進めてみる。以下、何も記述せずにページ数を出す場合はこの本のページを指す。
参考書籍: 独習アセンブラ 新版 (2021) 翔泳社
CファイルからSファイルの生成
まずは、ft_strlen.sを作成することを目指す。
ft_strlenは、const char *型の引数を取り、その文字列の長さをsize_tで返すものである。
C言語でft_strlenを実装してみると、以下のようになる。
typedef unsigned long size_t;
size_t my_strlen(const char *s) {
size_t l;
for (l = 0; s[l] != '\0'; l++);
return l;
}これを、gcc -S -O2 -fno-pie -fomit-frame-pointer my_strlen.cで.sファイルにすると、以下のようになった。
オプションについて
-S:アセンブラを出力-O2:最適化-fno-pie:位置非依存のコード(PIC)を生成しない-fomit-frame-pointer:フレームポインターを管理するコードを生成しない-fno-builtin:ビルトイン関数を使用しない
-fno-pieおよび-fomit-frame-pointerを入れなくても、生成物にほとんど変化はなかったため、ここでは意味がないかもしれない。変化があったのは、call strlen@PLTの@PLTがついたかどうかのみ。
.file "my_strlen.c"
.text
.p2align 4
.globl my_strlen
.type my_strlen, @function
my_strlen:
.LFB0:
.cfi_startproc
endbr64
xorl %eax, %eax
cmpb $0, (%rdi)
je .L4
.p2align 4,,10
.p2align 3
.L3:
addq $1, %rax
cmpb $0, (%rdi,%rax)
jne .L3
ret
.p2align 4,,10
.p2align 3
.L4:
ret
.cfi_endproc
.LFE0:
.size my_strlen, .-my_strlen
.ident "GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:ちなみに、Docker上(docker run --platform=linux/amd64 -it -v "$(pwd)":/workspace ubuntu /bin/bash)で実行している。
gdbでのトレース
C言語ソースコードからアセンブリを生成し、gdbでトレースする。
.gdbinitは以下のように記述しておく。
break *main
display /x $eax
display /x $ebx
display /x $esi
display /4i $pcちなみに、セキュリティ上の関係で、以下を指定されたファイル(ホームディレクトリの.gdbinit?)に書き込まなければいけない場合がある。
set auto-load safe-path /
ところが、Apple Siliconではgdbによるデバッグができなかった。参考
解決策として、以下のようなものもあるが、.gdbinitの内容を毎回ロードさせなければいけないため、gdbは一旦諦めることとした。
ROSETTA_DEBUGSERVER_PORT=1234 ./my_binary & gdb
(gdb) set architecture i386:x86-64
(gdb) file my_binary
(gdb) target remote localhost:1234
追記:Apple SiliconでもPaiza Cloudでx86_64環境が使えるので、ソースコードをアップロードしてデバッグできた。
GNU アセンブラ
前提知識を飛ばして、p.182からのGAS(GNUアセンブラ)からの説明を読み進める。
以下はGNU binutilsに含まれる主なコマンド(p.183 表7.1 から抜粋)。
| コマンド | 機能 |
|---|---|
| ar | アーカイブの作成・変更・操作 |
| nm | オブジェクトファイル中のシンボル一覧 |
| objcpy | オブジェクトファイルの複製・変換 |
| objdump | オブジェクトファイルの情報表示 |
| ranlib | アーカイブの索引生成 |
| size | オブジェクトファイル・アーカイブのセクションの大きさを表示 |
| strings | 文字列の抽出 |
| strip | オブジェクトファイル内のシンボルを削除する |
| c++filt | C++のシンボル名のdemangle |
| addr2line | アドレスをファイル名と行番号に変換 |
| readelf | ELFファイルの中身を表示 |
| elfedit | ELFファイルのヘッダの更新 |
面白いのは、objdump -M intel -d <.oファイル>でオブジェクトファイルを逆アセンブルできる。 |
アセンブラの文法
p.204で説明されているGASの文法をまとめると、
- スペースとタブ、インデント
- コメント
- シンボル
- 局所シンボル(
Lや.Lで始まるシンボル。オプジェクトファイルに記録されない。)
- 局所シンボル(
- 文
- ラベル(
main:など:で終わる)- 局所ラベル(
数字:の形式で記述される)
- 局所ラベル(
- 命令
- 定数
- 整数
- bignum
- flonum
- 文字
- 文字列
- セクション
- 式
となる。
しかし、上記のGASはAT&T構文であり、要件のIntel構文とは以下の点で異なる。
- オペランドの順番
- AT&Tでは、
命令 出所, 宛先のように記述する。 - Intelでは、
命令 宛先, 出所のように記述する。
- AT&Tでは、
- 命令の表記法
- AT&Tでは、ニーモニックのsuffixでデータサイズを指定する。
- Intelでは、オペランドのprefixでデータサイズを指定する。
- 即値及びレジスタの表記法
- 間接メモリ参照の記述法
間接メモリ参照
間接メモリ参照は、以下のようにアドレスを決定する(p.224)。
アドレス = ベースレジスタの値 + インデックスレジスタの値 * 倍率 + 変位
AT&T構文とIntel構文は間接アドレス参照のオペランドを
変位(ベースレジスタ, インデックスレジスタ, 倍率) # AT&T
[ベースレジスタ + インデックスレジスタ * 倍率 + 変位] # Intel
のように書く。以下のように記述される(Intel構文)。
mov dword ptr [ebp - 4], 123
mov dword ptr [eax * 4 + i], 123
mov word ptr [ebp + eax * 2 + 8], 123
それぞれ、指定されたアドレスに123を格納する。
擬似命令
p.228以降の説明をまとめる。これらの説明はAT&T構文のようなので、Intel構文に読み替える必要がある。
| 擬似命令 | 説明 |
|---|---|
.file | 論理ファイルの開始 |
.text | .textセクションの開始 |
.global <シンボル> | シンボルを他のオブジェクトファイルから参照可能にする |
.type <シンボル>, <type> | シンボルの種類をGASに伝える |
.cfi_... | Call Frame Information |
.cfa_... | Canonical Frame Address |
Call Frame Informationは、関数呼び出し時にスタックフレームをデバッガに知らせるための情報。 Canonical Frame Addressは、関数呼び出し元のスタックフレームのアドレス。
命令ランキング
pp.273-275で、/usr/bin内の実行ファイルにおける機械語の命令の出現回数を数える方法が紹介されていたので、自分の環境(Docker ubuntu:24.04)でも試してみた。
全体の結果はニーモニックランキングに示し、以下には上位15位までを示す。 この15個の命令だけで全体の約90%を占めていることがわかる。
| Rank | Pct | CumPct | Mnemonic |
|---|---|---|---|
| 1 | 35.131 | 35.131 | mov |
| 2 | 7.408 | 42.539 | call |
| 3 | 6.050 | 48.588 | cmp |
| 4 | 5.268 | 53.856 | je |
| 5 | 5.194 | 59.050 | test |
| 6 | 5.009 | 64.060 | jmp |
| 7 | 4.110 | 68.169 | lea |
| 8 | 3.963 | 72.132 | jne |
| 9 | 3.355 | 75.487 | pop |
| 10 | 3.265 | 78.752 | xor |
| 11 | 3.174 | 81.926 | push |
| 12 | 2.061 | 83.987 | add |
| 13 | 1.976 | 85.963 | nop |
| 14 | 1.642 | 87.605 | movzx |
| 15 | 1.537 | 89.142 | sub |
他の文献へ移動
AT&T記法で本を読み進めるのも限界を感じてきたので、Intel記法の情報源を探すことに。
すると、ブログアセンブリを読むための基礎知識 | 晴耕雨読を見つけた。また、そのブログが参考文献にしていたIntel® 64 and IA-32 Architectures Software Developer Manualsも見つけた(Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4をダウンロードした)。
BNF記法などで説明されたIntel記法の文法を求めていたが、マニュアルにはその記載はなさそうだった。 → nasmの公式サイトにあった。
ここで知ったのだが、以下二つは別物だった。nasmを使う用件から、下の情報源を探した方が良さそうだ。
- MASM(Microsoft Macro Assembler)
- NASM(Netwide Assembler)
ここで見つけた文献を整理する。
文献1. Intel Architectures
Intel® 64 and IA-32 Architectures Software Developer Manuals
命令セットやハードウェア、命令とEFlagsの関係についての説明がある。
文献2. ABI
System V Application Binary Interface AMD64 Architecture Processor Supplement
Calling Conventionやデータ型の配置についての説明がある。
文献3. NASM
nasmアセンブラの説明。nasmのオプションやマクロについての説明がある。
レジスタ
64bit環境では、以下のレジスタがある(文献1)。
- 64bitのレジスタ: RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP, R8-R15
- 32bitのレジスタ: EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP, R8D-R15D
- 16bitのレジスタ: AX, BX, CX, DX, SI, DI, SP, BP, R8W-R15W
- 8-bitのレジスタ
- REXプレフィックスあり: AL, BL, CL, DL, SIL, DIL, SPL, BPL, R8B-R15B
- REXプレフィックスなし: AL, BL, CL, DL, AH, BH, CH, DH
- Segmentレジスタ: CS, DS, SS, ES, FS, GS
- RFLAGSレジスタ
- X87 FPUレジスタ
- MMXレジスタ (MM0-MM7)
- XMMレジスタ (XMM0-XMM15), MXCSRレジスタ
- Controlレジスタ: CR0, CR2, CR3, CR4, CR8
- System table pointerレジスタ: GDTR, LDTR, IDTR, taskレジスタ
- Debugレジスタ: DR0, DR1, DR2, DR3, DR6, DR7
- MSRレジスタ
- 128bitオペランドのレジスタ (RDX:RAX)
ちなみに、charが8bit、intが32bit、longが64bit(文献2 17p)。
Calling Convention
文献2 p.20、3.2 Function Calling Sequenceに詳述される。
- 引数の型がINTEGER, SSE, MEMORYなどのクラスに分類される。
- 1の分類に従い、引数がレジスタおよびスタックに配置される。
Aggregate Type(struct, array, union)の分類:
- サイズが64Byteよりも大きいか、unalignedなフィールドがある場合→MEMORY
- C++のnon-trivialな型→invisible referenceとしてポインタ(INTEGER)
- 1, 2以外の場合は、8バイトごとのチャンクに分割しNO_CLASSにする。
- 3で各チャンクに複数のフィールドがまたがる場合は、
- 両方同じクラス→そのクラス
- 一方がNO_CLASS→もう一つのクラス
- 一方がMEMORY→MEMORY
- 一方がINTEGER
- …
- 4で一部にMemoryを含む場合は全体をMemoryにするなど。
Aggregate Typeの例
struct {int a; int b}は、INTEGER, INTEGERで全体が8バイトだから、1つのINTEGERチャンクに収まる。すなわち、1つのレジスタに配置され、下位4バイトが.aで上位4バイトが.bとなる(%ediが.aとなる。.bをとる場合は、32bitシフトしてから%ediをとる)。struct {int a; long b}は、longが8バイトにアラインメントされ、aとbの間に4バイトのパディングが入る。INTEGER+NO_CLASS, INTEGER→INTEGER2つのチャンクとなり、2つのレジスタに配置される。パディングが入ることはこのstructのsizeofをとり16になることで確認できる。
| クラス | 型の例 | 配置先 |
|---|---|---|
| INTEGER | char, int, long long, void * | %rdi, %rsi, %rdx, %rcx, %r8, %r9 |
| SSE | float, double | %xmm0から%xmm7 |
| SSEUP | 構造体内の連続したfloat | 直前のxxmレジスタの上位部分に追加 |
| NO_CLASS | (パディング) | |
| MEMORY | 大きな構造体など、上記以外 | アラインメントされてスタックに |
| 補足: |
- レジスタに収まりきらない場合は、スタックに積まれる
- Aggregate Typeの一部がレジスタに収まらない場合は全てスタックに積まれる(rollback)
- スタックは右から左に積まれる(右端が低アドレス、
%rspは最後に積まれた左端を指す)- スタックはアドレスの低い方(
%rsp)に向かって積まれる
- スタックはアドレスの低い方(
%alは可変長引数の個数を表す%r10はfunction’s static chain pointer(?)を表す- INTEGERで余ったbitの内容は未定義
- 例えば、int型(32bit)で余った上位32bitに何が入っているかは未定義
スタックの様子:
↑ 低アドレス側(pushで伸びる方向) │ │ [rsp - 128] ← red zone(割り込みで破壊されない128バイト) │ ... │ [rsp] ← ローカル変数領域の終了 │ ... │ [rbp - 8] ← ローカル変数領域の開始 │ [rbp] ← previous `%rbp` value │ [rbp + 8] ← return address・ここより上が現在のstack frame │ [rbp + 16] ← memory argument eightbyte 0 │ [rbp + 24] ← memory argument eightbyte 1 │ ... │ [rbp + 8n + 16]← memory argument eightbyte n │ ↓ 高アドレス側(引数、データセグメント等に向かう)7つ目以降のINTEGER引数はMEMORY引数として渡されることに注意。
calleeが%rbpに呼ばれた時点での%rspを保存しておくことで、%rbp経由でローカル変数などにアクセスできる。もちろん、sub rsp Nなどで%rspを動かし、関数の最後で戻す必要がある。
ローカル変数領域に対する
%rbpの活用例func: push rbp ; save %rbp (callee-saved register) mov rbp, rsp ; %rbp = %rsp sub rsp, N ; allocate N Byte for local variables ... mov rsp, rbp ; restore %rsp from %rbp pop rbp ; restore %rbp ret
返り値は以下の順で返される:
- 引数と同様に、型を分類する。
- クラスがMEMORYの場合:
- callerは事前に領域を確保しておき、最初の引数として
%rdiに渡している - calleeはその領域に返り値を書き込む
%raiにそのアドレスを再設定する
- callerは事前に領域を確保しておき、最初の引数として
- クラスがINTEGERの場合、
%rax(収まり切らない場合は%rdxにも)に書き込む - クラスがSSE/SSEUPの場合、
%xmm0(収まらなければ%xmm1にも)に書き込む
レジスタの使用用途
callee-savedなレジスタは、calleeが終了する前に呼び出された時の値を元に戻さなければいけないレジスタ。
| レジスタ | saved | 用途 |
|---|---|---|
%rax | No | %alで可変引数のvector registers使用数、返り値の1番目 |
%rbx | Yes | |
%rcx | No | 整数引数の4番目 |
%rdx | No | 整数引数の3番目、返り値の2番目 |
%rsp | Yes | スタックポインタ |
%rbp | Yes | フレームポインタ |
%rsi | No | 整数引数の2番目 |
%rdi | No | 整数引数の1番目 |
%r8 | No | 整数引数の5番目 |
%r9 | No | 整数引数の6番目 |
%r10 | No | function’s static chain pointer |
%r11 | No | |
%r12-%r15 | Yes | |
%r16-%r31 | No | |
%xmm0-%xmm1 | No | 浮動点少数の引数・返り値 |
%xmm2-%xmm7 | No | 浮動点少数の引数 |
%xmm8-%xmm15 | No |
Syscall
文献2にSyscallについての記述がある。
文献2 Appendix A. Linux Conventions(要約)
Linux kernelとuser-level applicationは以下の点でのみ異なる。
- INTEGER引数を渡す順番:
- user:
%rdi,%rsi,%rdx,%rcx,%r8,%r9- kernel:
%rdi,%rsi,%rdx,%r10,%r8,%r9
%raxにsyscall番号を設定し、syscall命令でシステムコールを呼び出す。- 引数は最大6個でスタックでは渡さない。カテゴリINTEGERとMEMORYのみが渡される。
%rcx,%r11は破壊され、それと%rax以外のすべてのレジスタは保持される。- syscallの後、
%raxには-errno(-4905〜-1)が格納される。
Syscall番号はtorvalds/linux - GitHubで確認できる?
また、errno_locationなどの外部関数を呼び出す場合の注意について、文献3で以下のように説明されている。
文献3 10.2.5 Calling Procedures Outside the Library(抜粋)
To call an external routine, you must use another special PIC relocation type,
WRT ..plt. This is much easier than the GOT-based ones: you simply replace calls such asCALL printfwith the PLT-relative versionCALL printf WRT ..plt.
つまり、以下のように呼び出す必要がある。
call __errno_location wrt ..plt命令たち
上で示した上位14位までの命令を整理する(全体はニーモニックランキング)。 nopは省略(バインディングやタイミング調整に使われる)。
文献1 5.1に概要が説明される。詳細な説明は文献1のVol.2 にある。
データ転送
| 命令 | 説明 |
|---|---|
| mov | レジスタ・メモリ間でデータをコピーする。 例: mov eax, ebx : ebx の値を eax にコピー。 |
| lea | メモリアドレスを計算してレジスタに格納する(ポインタのような使い方)。 例: lea eax, [ebx+4]:eax = ebx + 4に相当 |
| movzx | 小さいサイズの値を大きいレジスタにゼロ拡張してコピー。 例: movzx eax, byte ptr [ebx] |
| push | スタックにデータを格納する。 |
| pop | スタックからデータを取り出してレジスタに格納。 |
演算
| 命令 | 説明 |
|---|---|
| add | 加算。例:add eax, 1 は eax に 1 を加える。 |
| sub | 減算。例:sub eax, 1 は eax から 1 を引く。 |
| xor | 排他的論理和(XOR)。例:xor eax, eax は eax を 0 に初期化する用途が多い。 |
0初期化のためのmovとxorの違い
- 即値の分、xorの方がmovよりも命令サイズが小さい
- movはフラグを変更しないが、xorはZeroフラグをセットする。
論理・比較
| 命令 | 説明 |
|---|---|
| cmp | 2つの値を比較(内部的には減算だけして結果は保存しない)。 例: cmp eax, ebx |
| test | 論理ANDを行ってフラグだけを設定。 例: test eax, eax はゼロかどうかのチェックに使われる。 |
制御
| 命令 | 説明 |
|---|---|
| call | 関数呼び出し。呼び出し元アドレスをスタックに保存し、指定先へジャンプ。 |
| jmp | 無条件ジャンプ。例:jmp label |
| je | 比較後、等しい(Zero Flag=1)ならジャンプ。 |
| jne | 比較後、等しくない(Zero Flag=0)ならジャンプ。 |
Status Flags
文献1 Vol.1 3-17, 3.4.3.1に計算結果のフラグについて説明されている。
| Flag | bit | 説明 |
|---|---|---|
| Carry | 0 | 符号なし演算のオーバーフロー(繰り上がり・繰り下がり) |
| Parity | 2 | 計算結果の最下位Byteにおけるbitの1の数が偶数かどうか |
| Auxiliary Carry | 4 | 4ビット単位でのキャリーが発生したらセット |
| Zero | 6 | 計算結果がゼロならセット |
| Sign | 7 | 計算結果の最上位bitが1かどうか |
| Overflow | 11 | 符号あり演算のオーバーフロー(型の範囲を超えた) |
Assembler Directives
文献3 Chapter 7にassembler directiveの一覧が載っている。
- Section 7.1:
BITS: Target Processor Mode- Section 7.1.1:
USE16&USE32: Aliases for BITS
- Section 7.1.1:
- Section 7.2:
DEFAULT: Change the assembler defaults- Section 7.2.1:
REL&ABS: RIP-relative addressing - Section 7.2.2:
BND&NOBND:BNDprefix
- Section 7.2.1:
- Section 7.3:
SECTIONorSEGMENT: Changing and Defining Sections- Section 7.3.1: The
__?SECT?__Macro
- Section 7.3.1: The
- Section 7.4:
ABSOLUTE: Defining Absolute Labels - Section 7.5:
EXTERN: Importing Symbols from Other Modules - Section 7.6:
REQUIRED: Unconditionally Importing Symbols from Other Modules - Section 7.7:
GLOBAL: Exporting Symbols to Other Modules - Section 7.8:
COMMON: Defining Common Data Areas - Section 7.9:
STATIC: Local Symbols within Modules - Section 7.10:
(G|L)PREFIX,(G|L)POSTFIX: Mangling Symbols - Section 7.11:
CPU: Defining CPU Dependencies - Section 7.12:
FLOAT: Handling of floating-point constants - Section 7.13:
[WARNING]: Enable or disable warnings
Unwinding
文献2 6.3 Unwinding through assembler codeに、
For successful unwinding on AMD64 every function must provide a valid debug information in the DWARF Debugging Information Format. (中略) for hand-written assembly routines the debug info must be provided by the author of the code.
(再掲) Call Frame Informationは、関数呼び出し時にスタックフレームをデバッガに知らせるための情報。 Canonical Frame Addressは、関数呼び出し元のスタックフレームのアドレス。
原則として、%rspを動かしたら(関数呼び出しも含め)、CFIをつけておく。しかしnasmでは必要はなさそう?
文献2 Figure 6.2 Examples for Unwinding in Assembler
# - function with local variable allocated on the stack .type func_locvars,@function func_locvars: .cfi_startproc # allocate space for local vars sub $0x1234, %rsp .cfi_adjust_cfa_offset 0x1234 # body ... # release space of local vars and return add $0x1234, %rsp .cfi_adjust_cfa_offset -0x1234 ret .cfi_endproc # - function that moves frame pointer to another register # and then allocates space for local variables .type func_otherreg,@function func_otherreg: .cfi_startproc # save frame pointer to r12 movq %rsp, %r12 .cfi_def_cfa_register r12 # allocate space for local vars # (no .cfi_{def,adjust}_cfa_offset needed here, # because CFA is computed from r12!) sub $100,%rsp # body ... # restore frame pointer from r12 movq %r12, %rsp .cfi_def_cfa_register rsp ret .cfi_endproc
GNU stack
リンク時に以下のエラーが発生する。
/usr/bin/ld: warning: ft_strlen.o: missing .note.GNU-stack section implies executable stack
/usr/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker
.note.GNU-stackセクションを書かないと、stackが実行可能になってしまう(セキュリティ上の懸念)らしい。
Hardened/GNU stack quickstartによると、単に以下をペーストすれば良いとあるが、これにはプリプロセスを必要とする。
%ifidn __OUTPUT_FORMAT__,elf
section .note.GNU-stack noalloc noexec nowrite progbits
%endif
%ifidn __OUTPUT_FORMAT__,elf32
section .note.GNU-stack noalloc noexec nowrite progbits
%endif
%ifidn __OUTPUT_FORMAT__,elf64
section .note.GNU-stack noalloc noexec nowrite progbits
%endif要件で.sファイルとして提出する必要があるため、これは使えない。単に、section .note.GNU-stack noalloc noexec nowrite progbitsと書くことにする。
課題を進める
テスト駆動開発?で進める。 最初にmainでテストケースを作り、それに合うことを確認しながら進める。
- strlen
- strcpy
- strcmp
- write
- read
- strdup
続いて、自作関数の開発。
typedef struct s_list {
void *content;
struct s_list *next;
} t_list;- atoi_base(C-05)
baseで示される文字列で表現されたと仮定する文字列strをint型に直す。base = "0123456789"なら普通の10進数。base = "0123456789abcdef"なら16進数。base = "FT", str = "TTFFT"なら2進数の0b11001、つまり25。
strは任意の数のwhitespace、任意の数の符号が、この順で数より前に来ても良い。- 符号は
-が奇数回なら負の値に、偶数回なら正の値にする。
- 符号は
strがこのルールに従わなくなる直前までで数を解釈する。baseが普通の10進数でstr = " ---+--+1234ab567"は-1234となる。
- オーバーフローとアンダーフローは無視する(未定義)。
- 以下の場合はエラーとして
0を返す。strもしくはbaseがNULLの場合。baseが空文字もしくは1文字しか含まれていない場合。baseが同じ文字を2度含む場合。baseが符号やwhitespaceを含む場合。
- 文献3 3.4.2 Character Stringsにリテラルの表記法が載っている。
int ft_atoi_base(char *str, char *base);- ft_list_push_front(C12-01)
data(NULLでも良い)からt_list型のnodeをリストの先頭につける。*begin_listはそのnodeのnextとなる(NULLでも良い)。begin_listがNULLの場合、エラーとして何も行わない。
void ft_list_push_front(t_list **begin_list, void *data);- ft_list_size(C12-02)
begin_listから始まるリストのノード数を返す。- 返り値はint型なんですね。
int ft_list_size(t_list *begin_list);- ft_list_sort(C12-14)
cmp関数を用いて、リストを昇順に並び替える。cmpあるいはbegin_listがNULLの場合は、エラーとして何もしない。- 先頭が入れ替わることで、
*begin_listの値が変化する場合がある。
void ft_list_sort(t_list **begin_list, int (*cmp)());
(*cmp)(list->data, list_other_ptr->data);ft_list_sortは複雑なので、方針をここに示しておく。
void ft_list_sort(
t_list **begin_list, // [rsp]
int (*cmp)(void*,void*) // [rsp + 8]
)
{
if (!begin_list || !cmp)
return;
size_t size = list_size(*begin_list);
if (size < 2)
return;
size_t n_left = size - 1; // rbx
do
{
size_t n_idx = 0; // r12
t_list **prev_next_ptr = begin_list; // r13
t_list *current = *begin_list; // r14
t_list *next = current->next; // r15
do
{
int ret = cmp(current->content, next->content);
if (ret > 0)
{
// 両者の->nextを入れ替える
t_list *next_next = next->next; // rdx
current->next = next_next;
next->next = current;
// ...->next->current->...として扱う。
*prev_next_ptr = next;
prev_next_ptr = &(next->next);
next = next_next;
}
else
{
prev = &(current->next);
current = next;
next = next->next;
}
n_idx++;
} while (n_idx < n_left);
} while (--n_left > 0);
}- ft_list_remove_if(C12-12)
cmp関数にdata_refと掛けた返り値が0であるノード全てを削除する。t_listの中のメンバ変数dataは、free_fctがNULLでない場合、その関数にかけて削除する。t_list自体はfreeで削除する。cmpあるいはbegin_listがNULLの場合は、エラーとして何もしない。- 先頭が削除されることで、
*begin_listの値が変化する場合がある。
void ft_list_remove_if(t_list **begin_list, void *data_ref, int (*cmp)(), void (*free_fct)(void *));
(*cmp)(list_ptr->data, data_ref);
(*free_fct)(list_ptr->data);これも、方針をここに示す。
void ft_list_remove_if(
t_list **begin_list,
void *data_ref, // [rsp]
int (*cmp)(void *, void *), // [rsp + 8]
void (*free_fct)(void *) // r15
)
{
if (!begin_list || !cmp) return;
t_list **prev_ptr = begin_list; // r12
t_list *current = *begin_list; // r13
while (!current)
{
int ret = cmp(current->content, data_ref);
if (ret)
{
t_list *del_node = current; // rdi
void *del_content = current->content; // r14
current = current->next;
*prev_ptr = current;
free(del_node);
if (free_fct)
free_fct(del_content);
}
else
{
prev_ptr = ¤t->next;
current = current->next;
}
}
}